From: David Polakovic
Date: Tue, 24 Feb 2026 14:47:16 +0000 (+0100)
Subject: feat: Initial commit since server migration.
X-Git-Tag: 1.0.0
X-Git-Url: https://git.dpolakovic.space/%22%22.esc_url%28%24base_url%29.%22//%22%24link/%22/%22%22.esc_url%28%24base_url%29.%22//%22%24link/%22?a=commitdiff_plain;h=7be159980aeda58ae94a98785ed9134538a07448;p=dpolakovic-space
feat: Initial commit since server migration.
---
7be159980aeda58ae94a98785ed9134538a07448
diff --git a/Git-server/Coffee/coffee-counter.js b/Git-server/Coffee/coffee-counter.js
new file mode 100644
index 0000000..7bf3b7f
--- /dev/null
+++ b/Git-server/Coffee/coffee-counter.js
@@ -0,0 +1,119 @@
+/**
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2025 David Polakovic
+ *
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this page.
+ *
+ */
+
+// coffee-counter.js - Reusable coffee counter script with timezone-aware counting
+
+(function() {
+ // Configuration
+ const API_URL = 'https://git.dpolakovic.space';
+ const REFRESH_INTERVAL = 5000; // 5 seconds
+
+ // Get user's local midnight timestamp
+ function getLocalMidnight() {
+ const now = new Date();
+ const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
+ return Math.floor(midnight.getTime() / 1000); // Convert to Unix timestamp
+ }
+
+ // Function to update all counter elements
+ async function updateCounters() {
+ try {
+ const response = await fetch(`${API_URL}/pot0`, {
+ method: 'STATS',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch coffee stats');
+ }
+
+ const data = await response.json();
+ const allBrews = data.brews || []; // Array of Unix timestamps
+
+ // Filter brews since user's local midnight
+ const userMidnight = getLocalMidnight();
+ const todayBrews = allBrews.filter(timestamp => timestamp >= userMidnight);
+ const count = todayBrews.length;
+
+ console.log('User midnight:', new Date(userMidnight * 1000));
+ console.log('Total brews in 48h:', allBrews.length);
+ console.log('Brews since user midnight:', count);
+
+ // Update simple counters (just the number)
+ const simpleElements = document.querySelectorAll('.coffee-counter');
+ simpleElements.forEach(element => {
+ element.textContent = count;
+ element.classList.remove('loading');
+ });
+
+ // Update verbose counters (full sentence with HTML support)
+ const verboseElements = document.querySelectorAll('.coffee-counter-verbose');
+ verboseElements.forEach(element => {
+ let html;
+ if (count === 0) {
+ html = ', but served 0 coffees today. Do you want a coffee?';
+ } else if (count === 1) {
+ html = ' but served just 1 coffee today. Do you want a coffee too?';
+ } else {
+ html = ` and served ${count} coffees today. Do you want a coffee?`;
+ }
+ element.innerHTML = html;
+ element.classList.remove('loading');
+ });
+
+ } catch (error) {
+ console.error('Coffee counter error:', error);
+
+ // Mark simple counters as error
+ const simpleElements = document.querySelectorAll('.coffee-counter');
+ simpleElements.forEach(element => {
+ element.textContent = '?';
+ element.classList.add('error');
+ });
+
+ // Mark verbose counters as error
+ const verboseElements = document.querySelectorAll('.coffee-counter-verbose');
+ verboseElements.forEach(element => {
+ element.innerHTML = 'error loading counter';
+ element.classList.add('error');
+ });
+ }
+ }
+
+ // Wait for DOM to be ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ updateCounters();
+ setInterval(updateCounters, REFRESH_INTERVAL);
+ });
+ } else {
+ updateCounters();
+ setInterval(updateCounters, REFRESH_INTERVAL);
+ }
+})();
\ No newline at end of file
diff --git a/Git-server/Coffee/coffee-server.php b/Git-server/Coffee/coffee-server.php
new file mode 100644
index 0000000..6142ea9
--- /dev/null
+++ b/Git-server/Coffee/coffee-server.php
@@ -0,0 +1,529 @@
+ 'cream',
+ 'half-and-half' => 'half-and-half',
+ 'whole-milk' => 'whole milk',
+ 'part-skim' => 'part skim',
+ 'skim-milk' => 'skim milk',
+ 'non-dairy' => 'non-dairy',
+ 'oat-milk' => 'oat milk',
+ 'pea-milk' => 'pea milk',
+ 'rye-milk' => 'rye milk',
+ 'almond-milk' => 'almond milk',
+ 'soy-milk' => 'soy milk',
+ 'coconut-milk' => 'coconut milk',
+ 'pumpkin-puree' => 'pumpkin puree',
+ 'marple-syrup' => 'marple syrup',
+ 'vanilla' => 'vanilla',
+ 'cinnamon' => 'cinnamon',
+ 'raspberry' => 'raspberry',
+ 'chocolate' => 'chocolate',
+ 'whisky' => 'whisky',
+ 'rum' => 'rum',
+ 'kahlua' => 'Kahlua',
+ 'aquavit' => 'Aquavit'
+];
+
+// Max total additions capacity in ml
+$maxAdditionsCapacity = 200;
+
+$counterFile = '/var/www/html/dpolakovic.git/coffee/coffee_counter.json';
+$potStateFile = '/var/www/html/dpolakovic.git/coffee/pot_state.json';
+
+// Canonical pot identifiers used as keys in state files
+$potNames = ['pot-0', 'pot-1'];
+
+$potDisplayNames = [
+ 'pot-0' => 'red coffee pot',
+ 'pot-1' => 'white coffee pot'
+];
+
+$potDetails = [
+ 'pot-0' => 'brand new coffee pot in scarlett red color',
+ 'pot-1' => 'older, but still usable white coffee pot, used when red one is not available'
+];
+
+$potCapacities = [
+ 'pot-0' => 6,
+ 'pot-1' => 8
+];
+
+$totalPots = count($potNames);
+
+// Map every accepted URL slug to its canonical pot name
+// Parentheses in pot-(-2147483648) are an integer underflow joke
+$potAliases = [
+ 'pot-0' => 'pot-0',
+ 'pot0' => 'pot-0',
+ 'pot-2' => 'pot-0',
+ 'pot2' => 'pot-0',
+ 'pot--2147483648 ' => 'pot-0',
+ 'pot-n2147483648 ' => 'pot-0',
+ 'red-coffee-pot' => 'pot-0',
+ 'pot-1' => 'pot-1',
+ 'pot1' => 'pot-1',
+ 'white-coffee-pot' => 'pot-1'
+];
+
+$allPotSlugs = array_keys($potAliases);
+
+// ==========================================================
+// POT STATE FUNCTIONS
+// ==========================================================
+
+// State machine: ready -> being cleaned -> empty -> ready
+// Transitions are time-based or triggered by POST (refill)
+
+// Load pot state from JSON file, create default if missing
+function loadPotState() {
+ global $potStateFile, $potNames, $potCapacities;
+
+ if (!file_exists($potStateFile)) {
+ $state = [];
+ foreach ($potNames as $name) {
+ $state[$name] = [
+ 'status' => 'ready',
+ 'coffees_left' => $potCapacities[$name],
+ 'last_used' => 0,
+ 'state_change_time' => 0
+ ];
+ }
+ file_put_contents($potStateFile, json_encode($state));
+ return $state;
+ }
+
+ return json_decode(file_get_contents($potStateFile), true);
+}
+
+// Persist pot state to JSON file
+function savePotState($state) {
+ global $potStateFile;
+ file_put_contents($potStateFile, json_encode($state));
+}
+
+// Advance pot states based on elapsed time
+// Uses elseif to prevent double-transition in one request
+// Skips auto-refill for BREW target so empty pots refuse BREW
+function updatePotStatuses($state, $currentMethod = '', $targetPot = null) {
+ global $potNames, $potCapacities;
+
+ foreach ($potNames as $name) {
+ $elapsed = time() - $state[$name]['state_change_time'];
+
+ if ($state[$name]['status'] === 'being cleaned' && $elapsed >= 10) {
+ $state[$name]['status'] = 'empty';
+ $state[$name]['state_change_time'] = time();
+ } elseif ($state[$name]['status'] === 'empty' && $elapsed >= 60) {
+ if ($currentMethod === 'BREW' && ($targetPot === $name || $targetPot === null)) {
+ continue;
+ }
+ $state[$name]['status'] = 'ready';
+ $state[$name]['coffees_left'] = $potCapacities[$name];
+ $state[$name]['state_change_time'] = time();
+ }
+ }
+
+ return $state;
+}
+
+// ==========================================================
+// COUNTER FUNCTIONS
+// ==========================================================
+
+// Load brew timestamps and prune entries older than 48 hours
+function getBrewHistory() {
+ global $counterFile;
+
+ if (!file_exists($counterFile)) {
+ $data = ['brews' => []];
+ file_put_contents($counterFile, json_encode($data));
+ return $data;
+ }
+
+ $data = json_decode(file_get_contents($counterFile), true);
+
+ $cutoffTime = time() - (48 * 60 * 60);
+ $data['brews'] = array_filter($data['brews'], function($timestamp) use ($cutoffTime) {
+ return $timestamp > $cutoffTime;
+ });
+
+ $data['brews'] = array_values($data['brews']);
+ file_put_contents($counterFile, json_encode($data));
+
+ return $data;
+}
+
+// Append current timestamp to brew history
+function addBrew() {
+ global $counterFile;
+
+ $data = getBrewHistory();
+ $data['brews'][] = time();
+
+ file_put_contents($counterFile, json_encode($data));
+
+ return count($data['brews']);
+}
+
+// ==========================================================
+// ADDITIONS PARSER
+// ==========================================================
+
+// Parse Accept-Additions header into array of name/q pairs
+// Splits on comma, extracts optional ;q= quality value
+function parseAdditions($acceptAdditions) {
+ if (empty($acceptAdditions)) {
+ return [];
+ }
+
+ $additions = [];
+ $parts = explode(',', $acceptAdditions);
+
+ foreach ($parts as $part) {
+ $part = trim($part);
+ preg_match('/([^;]+)(;q=([0-9.]+))?/', $part, $matches);
+ if (!empty($matches[1])) {
+ $addition = trim($matches[1]);
+ $quality = isset($matches[3]) ? floatval($matches[3]) : 1.0;
+ $additions[] = ['name' => $addition, 'q' => $quality];
+ }
+ }
+
+ usort($additions, function($a, $b) {
+ return $b['q'] <=> $a['q'];
+ });
+
+ return $additions;
+}
+
+// ==========================================================
+// RESOLVE POT FROM URI
+// ==========================================================
+
+// Match URI against all known pot slugs including parenthesized
+// Uses preg_quote so special chars like () are escaped properly
+$slugPattern = implode('|', array_map('preg_quote', $allPotSlugs));
+preg_match('/\/(' . $slugPattern . ')/', $requestUri, $matches);
+
+$potName = null;
+if (isset($matches[1]) && isset($potAliases[$matches[1]])) {
+ $potName = $potAliases[$matches[1]];
+}
+
+// ==========================================================
+// LOAD AND UPDATE STATE
+// ==========================================================
+
+$potState = loadPotState();
+$potState = updatePotStatuses($potState, $method, $potName);
+savePotState($potState);
+
+// ==========================================================
+// WHEN METHOD
+// ==========================================================
+
+// Return 501 for WHEN method per RFC 2324 section 3
+if ($method === 'WHEN') {
+ http_response_code(501);
+ header('Content-Type: text/plain');
+ echo "Error 501 WHEN Not Implemented. Use BREW with declared addition quantity according to RFC2324.\n";
+ exit;
+}
+
+// ==========================================================
+// PROPFIND ON ROOT (discover all pots)
+// ==========================================================
+
+// Return server capabilities and pot listing when no pot specified
+// STATS method intentionally omitted from listed methods
+if ($method === 'PROPFIND' && $potName === null) {
+ http_response_code(200);
+ header('Content-Type: application/json');
+
+ $capabilities = [
+ 'protocol' => 'HTCPCP 1.0',
+ 'rfc' => '2324',
+ 'methods' => ['BREW', 'GET', 'POST', 'PROPFIND'],
+ 'additions_supported' => array_values($additionDisplayNames),
+ 'max_additions_capacity_ml' => $maxAdditionsCapacity,
+ 'total_pots' => $totalPots,
+ 'pots' => [
+ [
+ 'canonical' => 'pot-0',
+ 'aliases' => ['pot-0', 'pot0', 'pot-2', 'pot2', 'pot-(-2147483648)', 'red-coffee-pot'],
+ 'display_name' => $potDisplayNames['pot-0'],
+ 'capacity' => $potCapacities['pot-0']
+ ],
+ [
+ 'canonical' => 'pot-1',
+ 'aliases' => ['pot-1', 'pot1', 'white-coffee-pot'],
+ 'display_name' => $potDisplayNames['pot-1'],
+ 'capacity' => $potCapacities['pot-1']
+ ]
+ ],
+ 'auto_assignment' => 'Send BREW request without pot name for automatic assignment'
+ ];
+
+ echo json_encode($capabilities, JSON_PRETTY_PRINT) . "\n";
+ exit;
+}
+
+// ==========================================================
+// AUTO POT ASSIGNMENT
+// ==========================================================
+
+// Assign first available ready pot when BREW sent to root
+if ($potName === null && $method === 'BREW') {
+ $freePot = null;
+
+ foreach ($potNames as $name) {
+ if ($potState[$name]['status'] === 'ready') {
+ $freePot = $name;
+ break;
+ }
+ }
+
+ if ($freePot !== null) {
+ $potName = $freePot;
+ } else {
+ http_response_code(503);
+ header('Content-Type: text/plain');
+ echo "Error 503 No Coffee Pot Available. Try GET request for pot-0 or pot-1 to check their status.\n";
+ exit;
+ }
+}
+
+// ==========================================================
+// VALIDATE POT NAME
+// ==========================================================
+
+// Reject requests to unknown pot identifiers
+if ($potName === null || !in_array($potName, $potNames)) {
+ http_response_code(404);
+ header('Content-Type: text/plain');
+ $allAliases = implode(', ', $allPotSlugs);
+ echo "Error 404 Coffee Pot Not Found. Available pots: $allAliases\n";
+ exit;
+}
+
+$potStatus = $potState[$potName]['status'];
+$potDisplay = $potDisplayNames[$potName];
+
+// ==========================================================
+// METHOD SWITCH
+// ==========================================================
+
+switch ($method) {
+ case 'BREW':
+ // Refuse if pot is not ready (being cleaned or empty)
+ if ($potStatus !== 'ready') {
+ http_response_code(503);
+ header('Content-Type: text/plain');
+ echo "Error 503 Service Unavailable. The $potDisplay ($potName) is currently $potStatus.\n";
+ exit;
+ }
+
+ // Validate additions against supported list (case-insensitive)
+ $requestedAdditions = parseAdditions($acceptAdditions);
+ $approvedAdditions = [];
+ $unsupportedAdditions = [];
+
+ foreach ($requestedAdditions as $addition) {
+ $normalizedName = strtolower($addition['name']);
+ if (in_array($normalizedName, $availableAdditions)) {
+ $approvedAdditions[] = [
+ 'name' => $additionDisplayNames[$normalizedName],
+ 'q' => $addition['q']
+ ];
+ } else {
+ $unsupportedAdditions[] = $addition['name'];
+ }
+ }
+
+ if (!empty($unsupportedAdditions)) {
+ http_response_code(406);
+ header('Content-Type: text/plain');
+ echo "Error 406 Not Acceptable. We do not offer any " . implode(', ', $unsupportedAdditions) . ".\n";
+ exit;
+ }
+
+ // Reject if total addition volume exceeds cup capacity
+ // Sum of q values treated as ml
+ if (!empty($approvedAdditions)) {
+ $totalQ = 0;
+ foreach ($approvedAdditions as $add) {
+ $totalQ += $add['q'];
+ }
+ if ($totalQ > $maxAdditionsCapacity) {
+ http_response_code(507);
+ header('Content-Type: text/plain');
+ echo "Error 507 Insufficient (Cup) Storage. Our cups can handle up to {$maxAdditionsCapacity}ml of additions.\n";
+ exit;
+ }
+ }
+
+ // Decrement remaining coffees and trigger cleaning if empty
+ $potState[$potName]['coffees_left']--;
+ $potState[$potName]['last_used'] = time();
+
+ if ($potState[$potName]['coffees_left'] <= 0) {
+ $potState[$potName]['status'] = 'being cleaned';
+ $potState[$potName]['state_change_time'] = time();
+ }
+
+ savePotState($potState);
+ addBrew();
+
+ // Build and send brew response
+ sleep(1);
+ http_response_code(200);
+ header('Content-Type: message/coffeepot');
+ header('X-Coffee-Pot: ' . $potName);
+ header('X-Coffee-Pot-Name: ' . $potDisplay);
+
+ $additionNames = array_map(function($a) { return $a['name']; }, $approvedAdditions);
+
+ if (!empty($additionNames)) {
+ header('Additions: ' . implode(', ', $additionNames));
+ }
+
+ // Format additions list with "and" before last item
+ echo "Your coffee";
+ if (!empty($additionNames)) {
+ if (count($additionNames) === 1) {
+ $additionsStr = $additionNames[0];
+ } else {
+ $last = array_pop($additionNames);
+ $additionsStr = implode(', ', $additionNames) . ' and ' . $last;
+ }
+ echo " with " . $additionsStr;
+ }
+ echo " is ready from the $potDisplay ($potName). Enjoy!\n";
+ if ($potState[$potName]['status'] === 'being cleaned') {
+ echo "Note: $potDisplay is now being cleaned and will be available shortly.\n";
+ }
+ break;
+
+ // Return pot status as JSON
+ case 'GET':
+ http_response_code(200);
+ header('Content-Type: application/json');
+
+ $status = [
+ 'pot_id' => $potName,
+ 'pot_name' => $potDisplay,
+ 'status' => $potStatus,
+ 'coffees_left' => $potState[$potName]['coffees_left'],
+ 'capacity' => $potCapacities[$potName] . ' cups',
+ 'details' => $potDetails[$potName],
+ 'aliases' => array_keys(array_filter($potAliases, function($v) use ($potName) { return $v === $potName; }))
+ ];
+
+ echo json_encode($status, JSON_PRETTY_PRINT) . "\n";
+ break;
+
+ // Refill coffee pot (add beans)
+ // Only works when pot is empty, not during cleaning
+ case 'POST':
+ if ($potStatus === 'ready') {
+ http_response_code(200);
+ header('Content-Type: text/plain');
+ echo "$potDisplay ($potName) is already full and ready. No refill needed.\n";
+ exit;
+ }
+
+ if ($potStatus === 'being cleaned') {
+ http_response_code(503);
+ header('Content-Type: text/plain');
+ echo "Error 503 Service Unavailable. $potDisplay ($potName) is currently being cleaned. Please wait.\n";
+ exit;
+ }
+
+ $potState[$potName]['status'] = 'ready';
+ $potState[$potName]['coffees_left'] = $potCapacities[$potName];
+ $potState[$potName]['state_change_time'] = time();
+ savePotState($potState);
+
+ http_response_code(200);
+ header('Content-Type: text/plain');
+ echo "Coffee beans added to $potDisplay ($potName). Pot is now ready.\n";
+ break;
+
+ // Return pot capabilities as JSON
+ case 'PROPFIND':
+ http_response_code(200);
+ header('Content-Type: application/json');
+
+ $capabilities = [
+ 'pot_id' => $potName,
+ 'pot_name' => $potDisplay,
+ 'protocol' => 'HTCPCP 1.0',
+ 'rfc' => '2324',
+ 'methods' => ['BREW', 'GET', 'POST', 'PROPFIND'],
+ 'additions_supported' => array_values($additionDisplayNames),
+ 'max_additions_capacity_ml' => $maxAdditionsCapacity,
+ 'total_pots' => $totalPots,
+ 'pot_names' => $potNames,
+ 'auto_assignment' => 'Send BREW request without pot name for automatic assignment'
+ ];
+
+ echo json_encode($capabilities, JSON_PRETTY_PRINT) . "\n";
+ break;
+
+ // Return brew timestamps for counter widget
+ // CORS header needed for cross-origin JS fetch
+ // Not listed in PROPFIND methods to keep it internal
+ case 'STATS':
+ http_response_code(200);
+ header('Content-Type: application/json');
+ header('Access-Control-Allow-Origin: *');
+
+ $brewData = getBrewHistory();
+
+ echo json_encode([
+ 'brews' => $brewData['brews'],
+ 'total_48h' => count($brewData['brews'])
+ ], JSON_PRETTY_PRINT);
+ break;
+
+ default:
+ http_response_code(501);
+ header('Content-Type: text/plain');
+ echo "Error 501 Method $method Not Implemented\n";
+}
+?>
\ No newline at end of file
diff --git a/Git-server/Gitweb/banner.html b/Git-server/Gitweb/banner.html
new file mode 100644
index 0000000..f5399e4
--- /dev/null
+++ b/Git-server/Gitweb/banner.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Git-server/Gitweb/footer.html b/Git-server/Gitweb/footer.html
new file mode 100644
index 0000000..f52e5fd
--- /dev/null
+++ b/Git-server/Gitweb/footer.html
@@ -0,0 +1,18 @@
+
+
+ Copyright 2022-2026 David Polakovic. Individual project licenses are located in project
+ root in full length.
+
+ Site generated using Gitweb. Read
+ the documentation
+ for JavaScript and cookie information.
+ Additional source code available
+ here
+ under
+ GPLv3 license.
+
+ Server for this subdomain is
+ RFC 2324
+ compliant
+
+
diff --git a/Git-server/Gitweb/gitweb.cgi b/Git-server/Gitweb/gitweb.cgi
new file mode 100644
index 0000000..7987979
--- /dev/null
+++ b/Git-server/Gitweb/gitweb.cgi
@@ -0,0 +1,8558 @@
+#!/usr/bin/perl
+
+# gitweb - simple web interface to track changes in git repositories
+#
+# (C) 2005-2006, Kay Sievers
+# (C) 2005, Christian Gierke
+#
+# This program is licensed under the GPLv2
+
+use 5.008001;
+use strict;
+use warnings;
+# handle ACL in file access tests
+use filetest 'access';
+use CGI qw(:standard :escapeHTML -nosticky);
+use CGI::Util qw(unescape);
+use CGI::Carp qw(fatalsToBrowser set_message);
+use Encode;
+use Fcntl ':mode';
+use File::Find qw();
+use File::Basename qw(basename);
+use Time::HiRes qw(gettimeofday tv_interval);
+use Digest::MD5 qw(md5_hex);
+
+binmode STDOUT, ':utf8';
+
+if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) {
+ eval 'sub CGI::multi_param { CGI::param(@_) }'
+}
+
+our $t0 = [ gettimeofday() ];
+our $number_of_git_cmds = 0;
+
+BEGIN {
+ CGI->compile() if $ENV{'MOD_PERL'};
+}
+
+our $version = "2.47.3";
+
+our ($my_url, $my_uri, $base_url, $path_info, $home_link);
+sub evaluate_uri {
+ our $cgi;
+
+ our $my_url = $cgi->url();
+ our $my_uri = $cgi->url(-absolute => 1);
+
+ # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
+ # needed and used only for URLs with nonempty PATH_INFO
+ our $base_url = $my_url;
+
+ # When the script is used as DirectoryIndex, the URL does not contain the name
+ # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
+ # have to do it ourselves. We make $path_info global because it's also used
+ # later on.
+ #
+ # Another issue with the script being the DirectoryIndex is that the resulting
+ # $my_url data is not the full script URL: this is good, because we want
+ # generated links to keep implying the script name if it wasn't explicitly
+ # indicated in the URL we're handling, but it means that $my_url cannot be used
+ # as base URL.
+ # Therefore, if we needed to strip PATH_INFO, then we know that we have
+ # to build the base URL ourselves:
+ our $path_info = decode_utf8($ENV{"PATH_INFO"});
+ if ($path_info) {
+ # $path_info has already been URL-decoded by the web server, but
+ # $my_url and $my_uri have not. URL-decode them so we can properly
+ # strip $path_info.
+ $my_url = unescape($my_url);
+ $my_uri = unescape($my_uri);
+ if ($my_url =~ s,\Q$path_info\E$,, &&
+ $my_uri =~ s,\Q$path_info\E$,, &&
+ defined $ENV{'SCRIPT_NAME'}) {
+ $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
+ }
+ }
+
+ # target of the home link on top of all pages
+ our $home_link = $my_uri || "/";
+}
+
+# core git executable to use
+# this can just be "git" if your webserver has a sensible PATH
+our $GIT = "/usr/bin/git";
+
+# absolute fs-path which will be prepended to the project path
+#our $projectroot = "/pub/scm";
+our $projectroot = "/pub/git";
+
+# fs traversing limit for getting project list
+# the number is relative to the projectroot
+our $project_maxdepth = 2007;
+
+# string of the home link on top of all pages
+our $home_link_str = $ENV{"SERVER_NAME"} ? "git://" . $ENV{"SERVER_NAME"} : "projects";
+
+# extra breadcrumbs preceding the home link
+our @extra_breadcrumbs = ();
+
+# name of your site or organization to appear in page titles
+# replace this with something more descriptive for clearer bookmarks
+our $site_name = ""
+ || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
+
+# html snippet to include in the section of each page
+our $site_html_head_string = "";
+# filename of html text to include at top of each page
+our $site_header = "banner.html";
+# html text to include at home page
+our $home_text = "indextext.html";
+# filename of html text to include at bottom of each page
+our $site_footer = "footer.html";
+
+# URI of stylesheets
+our @stylesheets = ("static/gitweb.css");
+# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
+our $stylesheet = undef;
+# URI of GIT logo (72x27 size)
+our $logo = "static/git-logo.png";
+# URI of GIT favicon, assumed to be image/png type
+our $favicon = "static/git-favicon.png";
+# URI of gitweb.js (JavaScript code for gitweb)
+our $javascript = "static/gitweb.js";
+
+# URI and label (title) of GIT logo link
+#our $logo_url = "https://www.kernel.org/pub/software/scm/git/docs/";
+#our $logo_label = "git documentation";
+our $logo_url = "https://git-scm.com/";
+our $logo_label = "git homepage";
+
+# source of projects list
+our $projects_list = "";
+
+# the width (in characters) of the projects list "Description" column
+our $projects_list_description_width = 50;
+
+# group projects by category on the projects list
+# (enabled if this variable evaluates to true)
+our $projects_list_group_categories = 0;
+
+# default category if none specified
+# (leave the empty string for no category)
+our $project_list_default_category = "";
+
+# default order of projects list
+# valid values are none, project, descr, owner, and age
+our $default_projects_order = "descr";
+
+# show repository only if this file exists
+# (only effective if this variable evaluates to true)
+our $export_ok = "";
+
+# don't generate age column on the projects list page
+our $omit_age_column = 0;
+
+# don't generate information about owners of repositories
+our $omit_owner=1;
+
+# show repository only if this subroutine returns true
+# when given the path to the project, for example:
+# sub { return -e "$_[0]/git-daemon-export-ok"; }
+our $export_auth_hook = undef;
+
+# only allow viewing of repositories also shown on the overview page
+our $strict_export = "";
+
+# list of git base URLs used for URL to where fetch project from,
+# i.e. full URL is "$git_base_url/$project"
+our @git_base_url_list = grep { $_ ne '' } ("");
+
+# default blob_plain mimetype and default charset for text/plain blob
+our $default_blob_plain_mimetype = 'text/plain';
+our $default_text_plain_charset = undef;
+
+# file to use for guessing MIME types before trying /etc/mime.types
+# (relative to the current git repository)
+our $mimetypes_file = undef;
+
+# assume this charset if line contains non-UTF-8 characters;
+# it should be valid encoding (see Encoding::Supported(3pm) for list),
+# for which encoding all byte sequences are valid, for example
+# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
+# could be even 'utf-8' for the old behavior)
+our $fallback_encoding = 'latin1';
+
+# rename detection options for git-diff and git-diff-tree
+# - default is '-M', with the cost proportional to
+# (number of removed files) * (number of new files).
+# - more costly is '-C' (which implies '-M'), with the cost proportional to
+# (number of changed files + number of removed files) * (number of new files)
+# - even more costly is '-C', '--find-copies-harder' with cost
+# (number of files in the original tree) * (number of new files)
+# - one might want to include '-B' option, e.g. '-B', '-M'
+our @diff_opts = ('-M'); # taken from git_commit
+
+# Disables features that would allow repository owners to inject script into
+# the gitweb domain.
+our $prevent_xss = 0;
+
+# Path to the highlight executable to use (must be the one from
+# http://andre-simon.de/zip/download.php due to assumptions about parameters and output).
+# Useful if highlight is not installed on your webserver's PATH.
+# [Default: highlight]
+our $highlight_bin = "highlight";
+
+# information about snapshot formats that gitweb is capable of serving
+our %known_snapshot_formats = (
+ # name => {
+ # 'display' => display name,
+ # 'type' => mime type,
+ # 'suffix' => filename suffix,
+ # 'format' => --format for git-archive,
+ # 'compressor' => [compressor command and arguments]
+ # (array reference, optional)
+ # 'disabled' => boolean (optional)}
+ #
+ 'tgz' => {
+ 'display' => 'tar.gz',
+ 'type' => 'application/x-gzip',
+ 'suffix' => '.tar.gz',
+ 'format' => 'tar',
+ 'compressor' => ['gzip', '-n']},
+
+ 'tbz2' => {
+ 'display' => 'tar.bz2',
+ 'type' => 'application/x-bzip2',
+ 'suffix' => '.tar.bz2',
+ 'format' => 'tar',
+ 'compressor' => ['bzip2']},
+
+ 'txz' => {
+ 'display' => 'tar.xz',
+ 'type' => 'application/x-xz',
+ 'suffix' => '.tar.xz',
+ 'format' => 'tar',
+ 'compressor' => ['xz'],
+ 'disabled' => 1},
+
+ 'zip' => {
+ 'display' => 'zip',
+ 'type' => 'application/x-zip',
+ 'suffix' => '.zip',
+ 'format' => 'zip'},
+);
+
+# Aliases so we understand old gitweb.snapshot values in repository
+# configuration.
+our %known_snapshot_format_aliases = (
+ 'gzip' => 'tgz',
+ 'bzip2' => 'tbz2',
+ 'xz' => 'txz',
+
+ # backward compatibility: legacy gitweb config support
+ 'x-gzip' => undef, 'gz' => undef,
+ 'x-bzip2' => undef, 'bz2' => undef,
+ 'x-zip' => undef, '' => undef,
+);
+
+# Pixel sizes for icons and avatars. If the default font sizes or lineheights
+# are changed, it may be appropriate to change these values too via
+# $GITWEB_CONFIG.
+our %avatar_size = (
+ 'default' => 16,
+ 'double' => 32
+);
+
+# Used to set the maximum load that we will still respond to gitweb queries.
+# If server load exceed this value then return "503 server busy" error.
+# If gitweb cannot determined server load, it is taken to be 0.
+# Leave it undefined (or set to 'undef') to turn off load checking.
+our $maxload = 300;
+
+# configuration for 'highlight' (http://andre-simon.de/doku/highlight/en/highlight.php)
+# match by basename
+our %highlight_basename = (
+ #'Program' => 'py',
+ #'Library' => 'py',
+ 'SConstruct' => 'py', # SCons equivalent of Makefile
+ 'Makefile' => 'make',
+);
+# match by extension
+our %highlight_ext = (
+ # main extensions, defining name of syntax;
+ # see files in /usr/share/highlight/langDefs/ directory
+ (map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),
+ # alternate extensions, see /etc/highlight/filetypes.conf
+ (map { $_ => 'c' } qw(c h)),
+ (map { $_ => 'sh' } qw(sh bash zsh ksh)),
+ (map { $_ => 'cpp' } qw(cpp cxx c++ cc)),
+ (map { $_ => 'php' } qw(php php3 php4 php5 phps)),
+ (map { $_ => 'pl' } qw(pl perl pm)), # perhaps also 'cgi'
+ (map { $_ => 'make'} qw(make mak mk)),
+ (map { $_ => 'xml' } qw(xml xhtml html htm)),
+);
+
+# You define site-wide feature defaults here; override them with
+# $GITWEB_CONFIG as necessary.
+our %feature = (
+ # feature => {
+ # 'sub' => feature-sub (subroutine),
+ # 'override' => allow-override (boolean),
+ # 'default' => [ default options...] (array reference)}
+ #
+ # if feature is overridable (it means that allow-override has true value),
+ # then feature-sub will be called with default options as parameters;
+ # return value of feature-sub indicates if to enable specified feature
+ #
+ # if there is no 'sub' key (no feature-sub), then feature cannot be
+ # overridden
+ #
+ # use gitweb_get_feature() to retrieve the value
+ # (an array) or gitweb_check_feature() to check if
+ # is enabled
+
+ # Enable the 'blame' blob view, showing the last commit that modified
+ # each line in the file. This can be very CPU-intensive.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'blame'}{'default'} = [1];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'blame'}{'override'} = 1;
+ # and in project config gitweb.blame = 0|1;
+ 'blame' => {
+ 'sub' => sub { feature_bool('blame', @_) },
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Enable the 'snapshot' link, providing a compressed archive of any
+ # tree. This can potentially generate high traffic if you have large
+ # project.
+
+ # Value is a list of formats defined in %known_snapshot_formats that
+ # you wish to offer.
+ # To disable system wide have in $GITWEB_CONFIG
+ # $feature{'snapshot'}{'default'} = [];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'snapshot'}{'override'} = 1;
+ # and in project config, a comma-separated list of formats or "none"
+ # to disable. Example: gitweb.snapshot = tbz2,zip;
+ 'snapshot' => {
+ 'sub' => \&feature_snapshot,
+ 'override' => 0,
+ 'default' => ['tgz']},
+
+ # Enable text search, which will list the commits which match author,
+ # committer or commit text to a given string. Enabled by default.
+ # Project specific override is not supported.
+ #
+ # Note that this controls all search features, which means that if
+ # it is disabled, then 'grep' and 'pickaxe' search would also be
+ # disabled.
+ 'search' => {
+ 'override' => 0,
+ 'default' => [1]},
+
+ # Enable grep search, which will list the files in currently selected
+ # tree containing the given string. Enabled by default. This can be
+ # potentially CPU-intensive, of course.
+ # Note that you need to have 'search' feature enabled too.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'grep'}{'default'} = [1];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'grep'}{'override'} = 1;
+ # and in project config gitweb.grep = 0|1;
+ 'grep' => {
+ 'sub' => sub { feature_bool('grep', @_) },
+ 'override' => 0,
+ 'default' => [1]},
+
+ # Enable the pickaxe search, which will list the commits that modified
+ # a given string in a file. This can be practical and quite faster
+ # alternative to 'blame', but still potentially CPU-intensive.
+ # Note that you need to have 'search' feature enabled too.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'pickaxe'}{'default'} = [1];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'pickaxe'}{'override'} = 1;
+ # and in project config gitweb.pickaxe = 0|1;
+ 'pickaxe' => {
+ 'sub' => sub { feature_bool('pickaxe', @_) },
+ 'override' => 0,
+ 'default' => [1]},
+
+ # Enable showing size of blobs in a 'tree' view, in a separate
+ # column, similar to what 'ls -l' does. This cost a bit of IO.
+
+ # To disable system wide have in $GITWEB_CONFIG
+ # $feature{'show-sizes'}{'default'} = [0];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'show-sizes'}{'override'} = 1;
+ # and in project config gitweb.showsizes = 0|1;
+ 'show-sizes' => {
+ 'sub' => sub { feature_bool('showsizes', @_) },
+ 'override' => 0,
+ 'default' => [1]},
+
+ # Make gitweb use an alternative format of the URLs which can be
+ # more readable and natural-looking: project name is embedded
+ # directly in the path and the query string contains other
+ # auxiliary information. All gitweb installations recognize
+ # URL in either format; this configures in which formats gitweb
+ # generates links.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'pathinfo'}{'default'} = [1];
+ # Project specific override is not supported.
+
+ # Note that you will need to change the default location of CSS,
+ # favicon, logo and possibly other files to an absolute URL. Also,
+ # if gitweb.cgi serves as your indexfile, you will need to force
+ # $my_uri to contain the script name in your $GITWEB_CONFIG.
+ 'pathinfo' => {
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Make gitweb consider projects in project root subdirectories
+ # to be forks of existing projects. Given project $projname.git,
+ # projects matching $projname/*.git will not be shown in the main
+ # projects list, instead a '+' mark will be added to $projname
+ # there and a 'forks' view will be enabled for the project, listing
+ # all the forks. If project list is taken from a file, forks have
+ # to be listed after the main project.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'forks'}{'default'} = [1];
+ # Project specific override is not supported.
+ 'forks' => {
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Insert custom links to the action bar of all project pages.
+ # This enables you mainly to link to third-party scripts integrating
+ # into gitweb; e.g. git-browser for graphical history representation
+ # or custom web-based repository administration interface.
+
+ # The 'default' value consists of a list of triplets in the form
+ # (label, link, position) where position is the label after which
+ # to insert the link and link is a format string where %n expands
+ # to the project name, %f to the project path within the filesystem,
+ # %h to the current hash (h gitweb parameter) and %b to the current
+ # hash base (hb gitweb parameter); %% expands to %.
+
+ # To enable system wide have in $GITWEB_CONFIG e.g.
+ # $feature{'actions'}{'default'} = [('graphiclog',
+ # '/git-browser/by-commit.html?r=%n', 'summary')];
+ # Project specific override is not supported.
+ 'actions' => {
+ 'override' => 0,
+ 'default' => []},
+
+ # Allow gitweb scan project content tags of project repository,
+ # and display the popular Web 2.0-ish "tag cloud" near the projects
+ # list. Note that this is something COMPLETELY different from the
+ # normal Git tags.
+
+ # gitweb by itself can show existing tags, but it does not handle
+ # tagging itself; you need to do it externally, outside gitweb.
+ # The format is described in git_get_project_ctags() subroutine.
+ # You may want to install the HTML::TagCloud Perl module to get
+ # a pretty tag cloud instead of just a list of tags.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'ctags'}{'default'} = [1];
+ # Project specific override is not supported.
+
+ # In the future whether ctags editing is enabled might depend
+ # on the value, but using 1 should always mean no editing of ctags.
+ 'ctags' => {
+ 'override' => 0,
+ 'default' => [0]},
+
+ # The maximum number of patches in a patchset generated in patch
+ # view. Set this to 0 or undef to disable patch view, or to a
+ # negative number to remove any limit.
+
+ # To disable system wide have in $GITWEB_CONFIG
+ # $feature{'patches'}{'default'} = [0];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'patches'}{'override'} = 1;
+ # and in project config gitweb.patches = 0|n;
+ # where n is the maximum number of patches allowed in a patchset.
+ 'patches' => {
+ 'sub' => \&feature_patches,
+ 'override' => 0,
+ 'default' => [16]},
+
+ # Avatar support. When this feature is enabled, views such as
+ # shortlog or commit will display an avatar associated with
+ # the email of the committer(s) and/or author(s).
+
+ # Currently available providers are gravatar and picon.
+ # If an unknown provider is specified, the feature is disabled.
+
+ # Picon currently relies on the indiana.edu database.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'avatar'}{'default'} = [''];
+ # where is either gravatar or picon.
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'avatar'}{'override'} = 1;
+ # and in project config gitweb.avatar = ;
+ 'avatar' => {
+ 'sub' => \&feature_avatar,
+ 'override' => 0,
+ 'default' => ['']},
+
+ # Enable displaying how much time and how many git commands
+ # it took to generate and display page. Disabled by default.
+ # Project specific override is not supported.
+ 'timed' => {
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Enable turning some links into links to actions which require
+ # JavaScript to run (like 'blame_incremental'). Not enabled by
+ # default. Project specific override is currently not supported.
+ 'javascript-actions' => {
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Enable and configure ability to change common timezone for dates
+ # in gitweb output via JavaScript. Enabled by default.
+ # Project specific override is not supported.
+ 'javascript-timezone' => {
+ 'override' => 0,
+ 'default' => [
+ 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
+ # or undef to turn off this feature
+ 'gitweb_tz', # name of cookie where to store selected timezone
+ 'datetime', # CSS class used to mark up dates for manipulation
+ ]},
+
+ # Syntax highlighting support. This is based on Daniel Svensson's
+ # and Sham Chukoury's work in gitweb-xmms2.git.
+ # It requires the 'highlight' program present in $PATH,
+ # and therefore is disabled by default.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'highlight'}{'default'} = [1];
+
+ 'highlight' => {
+ 'sub' => sub { feature_bool('highlight', @_) },
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Enable displaying of remote heads in the heads list
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'remote_heads'}{'default'} = [1];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'remote_heads'}{'override'} = 1;
+ # and in project config gitweb.remoteheads = 0|1;
+ 'remote_heads' => {
+ 'sub' => sub { feature_bool('remote_heads', @_) },
+ 'override' => 0,
+ 'default' => [0]},
+
+ # Enable showing branches under other refs in addition to heads
+
+ # To set system wide extra branch refs have in $GITWEB_CONFIG
+ # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'extra-branch-refs'}{'override'} = 1;
+ # and in project config gitweb.extrabranchrefs = dirs of choice
+ # Every directory is separated with whitespace.
+
+ 'extra-branch-refs' => {
+ 'sub' => \&feature_extra_branch_refs,
+ 'override' => 0,
+ 'default' => []},
+
+ # Redact e-mail addresses.
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'email-privacy'}{'default'} = [1];
+ 'email-privacy' => {
+ 'sub' => sub { feature_bool('email-privacy', @_) },
+ 'override' => 1,
+ 'default' => [0]},
+);
+
+sub gitweb_get_feature {
+ my ($name) = @_;
+ return unless exists $feature{$name};
+ my ($sub, $override, @defaults) = (
+ $feature{$name}{'sub'},
+ $feature{$name}{'override'},
+ @{$feature{$name}{'default'}});
+ # project specific override is possible only if we have project
+ our $git_dir; # global variable, declared later
+ if (!$override || !defined $git_dir) {
+ return @defaults;
+ }
+ if (!defined $sub) {
+ warn "feature $name is not overridable";
+ return @defaults;
+ }
+ return $sub->(@defaults);
+}
+
+# A wrapper to check if a given feature is enabled.
+# With this, you can say
+#
+# my $bool_feat = gitweb_check_feature('bool_feat');
+# gitweb_check_feature('bool_feat') or somecode;
+#
+# instead of
+#
+# my ($bool_feat) = gitweb_get_feature('bool_feat');
+# (gitweb_get_feature('bool_feat'))[0] or somecode;
+#
+sub gitweb_check_feature {
+ return (gitweb_get_feature(@_))[0];
+}
+
+
+sub feature_bool {
+ my $key = shift;
+ my ($val) = git_get_project_config($key, '--bool');
+
+ if (!defined $val) {
+ return ($_[0]);
+ } elsif ($val eq 'true') {
+ return (1);
+ } elsif ($val eq 'false') {
+ return (0);
+ }
+}
+
+sub feature_snapshot {
+ my (@fmts) = @_;
+
+ my ($val) = git_get_project_config('snapshot');
+
+ if ($val) {
+ @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
+ }
+
+ return @fmts;
+}
+
+sub feature_patches {
+ my @val = (git_get_project_config('patches', '--int'));
+
+ if (@val) {
+ return @val;
+ }
+
+ return ($_[0]);
+}
+
+sub feature_avatar {
+ my @val = (git_get_project_config('avatar'));
+
+ return @val ? @val : @_;
+}
+
+sub feature_extra_branch_refs {
+ my (@branch_refs) = @_;
+ my $values = git_get_project_config('extrabranchrefs');
+
+ if ($values) {
+ $values = config_to_multi ($values);
+ @branch_refs = ();
+ foreach my $value (@{$values}) {
+ push @branch_refs, split /\s+/, $value;
+ }
+ }
+
+ return @branch_refs;
+}
+
+# checking HEAD file with -e is fragile if the repository was
+# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
+# and then pruned.
+sub check_head_link {
+ my ($dir) = @_;
+ my $headfile = "$dir/HEAD";
+ return ((-e $headfile) ||
+ (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
+}
+
+sub check_export_ok {
+ my ($dir) = @_;
+ return (check_head_link($dir) &&
+ (!$export_ok || -e "$dir/$export_ok") &&
+ (!$export_auth_hook || $export_auth_hook->($dir)));
+}
+
+# process alternate names for backward compatibility
+# filter out unsupported (unknown) snapshot formats
+sub filter_snapshot_fmts {
+ my @fmts = @_;
+
+ @fmts = map {
+ exists $known_snapshot_format_aliases{$_} ?
+ $known_snapshot_format_aliases{$_} : $_} @fmts;
+ @fmts = grep {
+ exists $known_snapshot_formats{$_} &&
+ !$known_snapshot_formats{$_}{'disabled'}} @fmts;
+}
+
+sub filter_and_validate_refs {
+ my @refs = @_;
+ my %unique_refs = ();
+
+ foreach my $ref (@refs) {
+ die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
+ # 'heads' are added implicitly in get_branch_refs().
+ $unique_refs{$ref} = 1 if ($ref ne 'heads');
+ }
+ return sort keys %unique_refs;
+}
+
+# If it is set to code reference, it is code that it is to be run once per
+# request, allowing updating configurations that change with each request,
+# while running other code in config file only once.
+#
+# Otherwise, if it is false then gitweb would process config file only once;
+# if it is true then gitweb config would be run for each request.
+our $per_request_config = 1;
+
+# read and parse gitweb config file given by its parameter.
+# returns true on success, false on recoverable error, allowing
+# to chain this subroutine, using first file that exists.
+# dies on errors during parsing config file, as it is unrecoverable.
+sub read_config_file {
+ my $filename = shift;
+ return unless defined $filename;
+ if (-e $filename) {
+ do $filename;
+ # die if there is a problem accessing the file
+ die $! if $!;
+ # die if there are errors parsing config file
+ die $@ if $@;
+ return 1;
+ }
+ return;
+}
+
+our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
+sub evaluate_gitweb_config {
+ our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl";
+ our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "/etc/gitweb.conf";
+ our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "/etc/gitweb-common.conf";
+
+ # Protect against duplications of file names, to not read config twice.
+ # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
+ # there possibility of duplication of filename there doesn't matter.
+ $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
+ $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
+
+ # Common system-wide settings for convenience.
+ # Those settings can be overridden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
+ read_config_file($GITWEB_CONFIG_COMMON);
+
+ # Use first config file that exists. This means use the per-instance
+ # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
+ read_config_file($GITWEB_CONFIG) and return;
+ read_config_file($GITWEB_CONFIG_SYSTEM);
+}
+
+# Get loadavg of system, to compare against $maxload.
+# Currently it requires '/proc/loadavg' present to get loadavg;
+# if it is not present it returns 0, which means no load checking.
+sub get_loadavg {
+ if( -e '/proc/loadavg' ){
+ open my $fd, '<', '/proc/loadavg'
+ or return 0;
+ my @load = split(/\s+/, scalar <$fd>);
+ close $fd;
+
+ # The first three columns measure CPU and IO utilization of the last one,
+ # five, and 10 minute periods. The fourth column shows the number of
+ # currently running processes and the total number of processes in the m/n
+ # format. The last column displays the last process ID used.
+ return $load[0] || 0;
+ }
+ # additional checks for load average should go here for things that don't export
+ # /proc/loadavg
+
+ return 0;
+}
+
+# version of the core git binary
+our $git_version;
+sub evaluate_git_version {
+ our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
+ $number_of_git_cmds++;
+}
+
+sub check_loadavg {
+ if (defined $maxload && get_loadavg() > $maxload) {
+ die_error(503, "The load average on the server is too high");
+ }
+}
+
+# ======================================================================
+# input validation and dispatch
+
+# Various hash size-related values.
+my $sha1_len = 40;
+my $sha256_extra_len = 24;
+my $sha256_len = $sha1_len + $sha256_extra_len;
+
+# A regex matching $len hex characters. $len may be a range (e.g. 7,64).
+sub oid_nlen_regex {
+ my $len = shift;
+ my $hchr = qr/[0-9a-fA-F]/;
+ return qr/(?:(?:$hchr){$len})/;
+}
+
+# A regex matching two sets of $nlen hex characters, prefixed by the literal
+# string $prefix and with the literal string $infix between them.
+sub oid_nlen_prefix_infix_regex {
+ my $nlen = shift;
+ my $prefix = shift;
+ my $infix = shift;
+
+ my $rx = oid_nlen_regex($nlen);
+
+ return qr/^\Q$prefix\E$rx\Q$infix\E$rx$/;
+}
+
+# A regex matching a valid object ID.
+our $oid_regex;
+{
+ my $x = oid_nlen_regex($sha1_len);
+ my $y = oid_nlen_regex($sha256_extra_len);
+ $oid_regex = qr/(?:$x(?:$y)?)/;
+}
+
+# input parameters can be collected from a variety of sources (presently, CGI
+# and PATH_INFO), so we define an %input_params hash that collects them all
+# together during validation: this allows subsequent uses (e.g. href()) to be
+# agnostic of the parameter origin
+
+our %input_params = ();
+
+# input parameters are stored with the long parameter name as key. This will
+# also be used in the href subroutine to convert parameters to their CGI
+# equivalent, and since the href() usage is the most frequent one, we store
+# the name -> CGI key mapping here, instead of the reverse.
+#
+# XXX: Warning: If you touch this, check the search form for updating,
+# too.
+
+our @cgi_param_mapping = (
+ project => "p",
+ action => "a",
+ file_name => "f",
+ file_parent => "fp",
+ hash => "h",
+ hash_parent => "hp",
+ hash_base => "hb",
+ hash_parent_base => "hpb",
+ page => "pg",
+ order => "o",
+ searchtext => "s",
+ searchtype => "st",
+ snapshot_format => "sf",
+ extra_options => "opt",
+ search_use_regexp => "sr",
+ ctag => "by_tag",
+ diff_style => "ds",
+ project_filter => "pf",
+ # this must be last entry (for manipulation from JavaScript)
+ javascript => "js"
+);
+our %cgi_param_mapping = @cgi_param_mapping;
+
+# we will also need to know the possible actions, for validation
+our %actions = (
+ "blame" => \&git_blame,
+ "blame_incremental" => \&git_blame_incremental,
+ "blame_data" => \&git_blame_data,
+ "blobdiff" => \&git_blobdiff,
+ "blobdiff_plain" => \&git_blobdiff_plain,
+ "blob" => \&git_blob,
+ "blob_plain" => \&git_blob_plain,
+ "commitdiff" => \&git_commitdiff,
+ "commitdiff_plain" => \&git_commitdiff_plain,
+ "commit" => \&git_commit,
+ "forks" => \&git_forks,
+ "heads" => \&git_heads,
+ "history" => \&git_history,
+ "log" => \&git_log,
+ "patch" => \&git_patch,
+ "patches" => \&git_patches,
+ "remotes" => \&git_remotes,
+ "rss" => \&git_rss,
+ "atom" => \&git_atom,
+ "search" => \&git_search,
+ "search_help" => \&git_search_help,
+ "shortlog" => \&git_shortlog,
+ "summary" => \&git_summary,
+ "tag" => \&git_tag,
+ "tags" => \&git_tags,
+ "tree" => \&git_tree,
+ "snapshot" => \&git_snapshot,
+ "object" => \&git_object,
+ # those below don't need $project
+ "opml" => \&git_opml,
+ "project_list" => \&git_project_list,
+ "project_index" => \&git_project_index,
+);
+
+# finally, we have the hash of allowed extra_options for the commands that
+# allow them
+our %allowed_options = (
+ "--no-merges" => [ qw(rss atom log shortlog history) ],
+);
+
+# fill %input_params with the CGI parameters. All values except for 'opt'
+# should be single values, but opt can be an array. We should probably
+# build an array of parameters that can be multi-valued, but since for the time
+# being it's only this one, we just single it out
+sub evaluate_query_params {
+ our $cgi;
+
+ while (my ($name, $symbol) = each %cgi_param_mapping) {
+ if ($symbol eq 'opt') {
+ $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
+ } else {
+ $input_params{$name} = decode_utf8($cgi->param($symbol));
+ }
+ }
+}
+
+# now read PATH_INFO and update the parameter list for missing parameters
+sub evaluate_path_info {
+ return if defined $input_params{'project'};
+ return if !$path_info;
+ $path_info =~ s,^/+,,;
+ return if !$path_info;
+
+ # find which part of PATH_INFO is project
+ my $project = $path_info;
+ $project =~ s,/+$,,;
+ while ($project && !check_head_link("$projectroot/$project")) {
+ $project =~ s,/*[^/]*$,,;
+ }
+ return unless $project;
+ $input_params{'project'} = $project;
+
+ # do not change any parameters if an action is given using the query string
+ return if $input_params{'action'};
+ $path_info =~ s,^\Q$project\E/*,,;
+
+ # next, check if we have an action
+ my $action = $path_info;
+ $action =~ s,/.*$,,;
+ if (exists $actions{$action}) {
+ $path_info =~ s,^$action/*,,;
+ $input_params{'action'} = $action;
+ }
+
+ # list of actions that want hash_base instead of hash, but can have no
+ # pathname (f) parameter
+ my @wants_base = (
+ 'tree',
+ 'history',
+ );
+
+ # we want to catch, among others
+ # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
+ my ($parentrefname, $parentpathname, $refname, $pathname) =
+ ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
+
+ # first, analyze the 'current' part
+ if (defined $pathname) {
+ # we got "branch:filename" or "branch:dir/"
+ # we could use git_get_type(branch:pathname), but:
+ # - it needs $git_dir
+ # - it does a git() call
+ # - the convention of terminating directories with a slash
+ # makes it superfluous
+ # - embedding the action in the PATH_INFO would make it even
+ # more superfluous
+ $pathname =~ s,^/+,,;
+ if (!$pathname || substr($pathname, -1) eq "/") {
+ $input_params{'action'} ||= "tree";
+ $pathname =~ s,/$,,;
+ } else {
+ # the default action depends on whether we had parent info
+ # or not
+ if ($parentrefname) {
+ $input_params{'action'} ||= "blobdiff_plain";
+ } else {
+ $input_params{'action'} ||= "blob_plain";
+ }
+ }
+ $input_params{'hash_base'} ||= $refname;
+ $input_params{'file_name'} ||= $pathname;
+ } elsif (defined $refname) {
+ # we got "branch". In this case we have to choose if we have to
+ # set hash or hash_base.
+ #
+ # Most of the actions without a pathname only want hash to be
+ # set, except for the ones specified in @wants_base that want
+ # hash_base instead. It should also be noted that hand-crafted
+ # links having 'history' as an action and no pathname or hash
+ # set will fail, but that happens regardless of PATH_INFO.
+ if (defined $parentrefname) {
+ # if there is parent let the default be 'shortlog' action
+ # (for http://git.example.com/repo.git/A..B links); if there
+ # is no parent, dispatch will detect type of object and set
+ # action appropriately if required (if action is not set)
+ $input_params{'action'} ||= "shortlog";
+ }
+ if ($input_params{'action'} &&
+ grep { $_ eq $input_params{'action'} } @wants_base) {
+ $input_params{'hash_base'} ||= $refname;
+ } else {
+ $input_params{'hash'} ||= $refname;
+ }
+ }
+
+ # next, handle the 'parent' part, if present
+ if (defined $parentrefname) {
+ # a missing pathspec defaults to the 'current' filename, allowing e.g.
+ # someproject/blobdiff/oldrev..newrev:/filename
+ if ($parentpathname) {
+ $parentpathname =~ s,^/+,,;
+ $parentpathname =~ s,/$,,;
+ $input_params{'file_parent'} ||= $parentpathname;
+ } else {
+ $input_params{'file_parent'} ||= $input_params{'file_name'};
+ }
+ # we assume that hash_parent_base is wanted if a path was specified,
+ # or if the action wants hash_base instead of hash
+ if (defined $input_params{'file_parent'} ||
+ grep { $_ eq $input_params{'action'} } @wants_base) {
+ $input_params{'hash_parent_base'} ||= $parentrefname;
+ } else {
+ $input_params{'hash_parent'} ||= $parentrefname;
+ }
+ }
+
+ # for the snapshot action, we allow URLs in the form
+ # $project/snapshot/$hash.ext
+ # where .ext determines the snapshot and gets removed from the
+ # passed $refname to provide the $hash.
+ #
+ # To be able to tell that $refname includes the format extension, we
+ # require the following two conditions to be satisfied:
+ # - the hash input parameter MUST have been set from the $refname part
+ # of the URL (i.e. they must be equal)
+ # - the snapshot format MUST NOT have been defined already (e.g. from
+ # CGI parameter sf)
+ # It's also useless to try any matching unless $refname has a dot,
+ # so we check for that too
+ if (defined $input_params{'action'} &&
+ $input_params{'action'} eq 'snapshot' &&
+ defined $refname && index($refname, '.') != -1 &&
+ $refname eq $input_params{'hash'} &&
+ !defined $input_params{'snapshot_format'}) {
+ # We loop over the known snapshot formats, checking for
+ # extensions. Allowed extensions are both the defined suffix
+ # (which includes the initial dot already) and the snapshot
+ # format key itself, with a prepended dot
+ while (my ($fmt, $opt) = each %known_snapshot_formats) {
+ my $hash = $refname;
+ unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
+ next;
+ }
+ my $sfx = $1;
+ # a valid suffix was found, so set the snapshot format
+ # and reset the hash parameter
+ $input_params{'snapshot_format'} = $fmt;
+ $input_params{'hash'} = $hash;
+ # we also set the format suffix to the one requested
+ # in the URL: this way a request for e.g. .tgz returns
+ # a .tgz instead of a .tar.gz
+ $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
+ last;
+ }
+ }
+}
+
+our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
+ $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
+ $searchtext, $search_regexp, $project_filter);
+sub evaluate_and_validate_params {
+ our $action = $input_params{'action'};
+ if (defined $action) {
+ if (!is_valid_action($action)) {
+ die_error(400, "Invalid action parameter");
+ }
+ }
+
+ # parameters which are pathnames
+ our $project = $input_params{'project'};
+ if (defined $project) {
+ if (!is_valid_project($project)) {
+ undef $project;
+ die_error(404, "No such project");
+ }
+ }
+
+ our $project_filter = $input_params{'project_filter'};
+ if (defined $project_filter) {
+ if (!is_valid_pathname($project_filter)) {
+ die_error(404, "Invalid project_filter parameter");
+ }
+ }
+
+ our $file_name = $input_params{'file_name'};
+ if (defined $file_name) {
+ if (!is_valid_pathname($file_name)) {
+ die_error(400, "Invalid file parameter");
+ }
+ }
+
+ our $file_parent = $input_params{'file_parent'};
+ if (defined $file_parent) {
+ if (!is_valid_pathname($file_parent)) {
+ die_error(400, "Invalid file parent parameter");
+ }
+ }
+
+ # parameters which are refnames
+ our $hash = $input_params{'hash'};
+ if (defined $hash) {
+ if (!is_valid_refname($hash)) {
+ die_error(400, "Invalid hash parameter");
+ }
+ }
+
+ our $hash_parent = $input_params{'hash_parent'};
+ if (defined $hash_parent) {
+ if (!is_valid_refname($hash_parent)) {
+ die_error(400, "Invalid hash parent parameter");
+ }
+ }
+
+ our $hash_base = $input_params{'hash_base'};
+ if (defined $hash_base) {
+ if (!is_valid_refname($hash_base)) {
+ die_error(400, "Invalid hash base parameter");
+ }
+ }
+
+ our @extra_options = @{$input_params{'extra_options'}};
+ # @extra_options is always defined, since it can only be (currently) set from
+ # CGI, and $cgi->param() returns the empty array in array context if the param
+ # is not set
+ foreach my $opt (@extra_options) {
+ if (not exists $allowed_options{$opt}) {
+ die_error(400, "Invalid option parameter");
+ }
+ if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
+ die_error(400, "Invalid option parameter for this action");
+ }
+ }
+
+ our $hash_parent_base = $input_params{'hash_parent_base'};
+ if (defined $hash_parent_base) {
+ if (!is_valid_refname($hash_parent_base)) {
+ die_error(400, "Invalid hash parent base parameter");
+ }
+ }
+
+ # other parameters
+ our $page = $input_params{'page'};
+ if (defined $page) {
+ if ($page =~ m/[^0-9]/) {
+ die_error(400, "Invalid page parameter");
+ }
+ }
+
+ our $searchtype = $input_params{'searchtype'};
+ if (defined $searchtype) {
+ if ($searchtype =~ m/[^a-z]/) {
+ die_error(400, "Invalid searchtype parameter");
+ }
+ }
+
+ our $search_use_regexp = $input_params{'search_use_regexp'};
+
+ our $searchtext = $input_params{'searchtext'};
+ our $search_regexp = undef;
+ if (defined $searchtext) {
+ if (length($searchtext) < 2) {
+ die_error(403, "At least two characters are required for search parameter");
+ }
+ if ($search_use_regexp) {
+ $search_regexp = $searchtext;
+ if (!eval { qr/$search_regexp/; 1; }) {
+ (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
+ die_error(400, "Invalid search regexp '$search_regexp'",
+ esc_html($error));
+ }
+ } else {
+ $search_regexp = quotemeta $searchtext;
+ }
+ }
+}
+
+# path to the current git repository
+our $git_dir;
+sub evaluate_git_dir {
+ our $git_dir = "$projectroot/$project" if $project;
+}
+
+our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
+sub configure_gitweb_features {
+ # list of supported snapshot formats
+ our @snapshot_fmts = gitweb_get_feature('snapshot');
+ @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
+
+ our ($git_avatar) = gitweb_get_feature('avatar');
+ $git_avatar = '' unless $git_avatar =~ /^(?:gravatar|picon)$/s;
+
+ our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
+ @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
+}
+
+sub get_branch_refs {
+ return ('heads', @extra_branch_refs);
+}
+
+# custom error handler: 'die ' is Internal Server Error
+sub handle_errors_html {
+ my $msg = shift; # it is already HTML escaped
+
+ # to avoid infinite loop where error occurs in die_error,
+ # change handler to default handler, disabling handle_errors_html
+ set_message("Error occurred when inside die_error:\n$msg");
+
+ # you cannot jump out of die_error when called as error handler;
+ # the subroutine set via CGI::Carp::set_message is called _after_
+ # HTTP headers are already written, so it cannot write them itself
+ die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
+}
+set_message(\&handle_errors_html);
+
+# dispatch
+sub dispatch {
+ if (!defined $action) {
+ if (defined $hash) {
+ $action = git_get_type($hash);
+ $action or die_error(404, "Object does not exist");
+ } elsif (defined $hash_base && defined $file_name) {
+ $action = git_get_type("$hash_base:$file_name");
+ $action or die_error(404, "File or directory does not exist");
+ } elsif (defined $project) {
+ $action = 'summary';
+ } else {
+ $action = 'project_list';
+ }
+ }
+ if (!defined($actions{$action})) {
+ die_error(400, "Unknown action");
+ }
+ if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
+ !$project) {
+ die_error(400, "Project needed");
+ }
+ $actions{$action}->();
+}
+
+sub reset_timer {
+ our $t0 = [ gettimeofday() ]
+ if defined $t0;
+ our $number_of_git_cmds = 0;
+}
+
+our $first_request = 1;
+sub run_request {
+ reset_timer();
+
+ evaluate_uri();
+ if ($first_request) {
+ evaluate_gitweb_config();
+ evaluate_git_version();
+ }
+ if ($per_request_config) {
+ if (ref($per_request_config) eq 'CODE') {
+ $per_request_config->();
+ } elsif (!$first_request) {
+ evaluate_gitweb_config();
+ }
+ }
+ check_loadavg();
+
+ # $projectroot and $projects_list might be set in gitweb config file
+ $projects_list ||= $projectroot;
+
+ evaluate_query_params();
+ evaluate_path_info();
+ evaluate_and_validate_params();
+ evaluate_git_dir();
+
+ configure_gitweb_features();
+
+ dispatch();
+}
+
+our $is_last_request = sub { 1 };
+our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
+our $CGI = 'CGI';
+our $cgi;
+our $FCGI_Stream_PRINT_raw = \&FCGI::Stream::PRINT;
+sub configure_as_fcgi {
+ require CGI::Fast;
+ our $CGI = 'CGI::Fast';
+ # FCGI is not Unicode aware hence the UTF-8 encoding must be done manually.
+ # However no encoding must be done within git_blob_plain() and git_snapshot()
+ # which must still output in raw binary mode.
+ no warnings 'redefine';
+ my $enc = Encode::find_encoding('UTF-8');
+ *FCGI::Stream::PRINT = sub {
+ my @OUTPUT = @_;
+ for (my $i = 1; $i < @_; $i++) {
+ $OUTPUT[$i] = $enc->encode($_[$i], Encode::FB_CROAK|Encode::LEAVE_SRC);
+ }
+ @_ = @OUTPUT;
+ goto $FCGI_Stream_PRINT_raw;
+ };
+
+ my $request_number = 0;
+ # let each child service 100 requests
+ our $is_last_request = sub { ++$request_number > 100 };
+}
+sub evaluate_argv {
+ my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
+ configure_as_fcgi()
+ if $script_name =~ /\.fcgi$/;
+
+ return unless (@ARGV);
+
+ require Getopt::Long;
+ Getopt::Long::GetOptions(
+ 'fastcgi|fcgi|f' => \&configure_as_fcgi,
+ 'nproc|n=i' => sub {
+ my ($arg, $val) = @_;
+ return unless eval { require FCGI::ProcManager; 1; };
+ my $proc_manager = FCGI::ProcManager->new({
+ n_processes => $val,
+ });
+ our $pre_listen_hook = sub { $proc_manager->pm_manage() };
+ our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
+ our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
+ },
+ );
+}
+
+sub run {
+ evaluate_argv();
+
+ $first_request = 1;
+ $pre_listen_hook->()
+ if $pre_listen_hook;
+
+ REQUEST:
+ while ($cgi = $CGI->new()) {
+ $pre_dispatch_hook->()
+ if $pre_dispatch_hook;
+
+ run_request();
+
+ $post_dispatch_hook->()
+ if $post_dispatch_hook;
+ $first_request = 0;
+
+ last REQUEST if ($is_last_request->());
+ }
+
+ DONE_GITWEB:
+ 1;
+}
+
+run();
+
+if (defined caller) {
+ # wrapped in a subroutine processing requests,
+ # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
+ return;
+} else {
+ # pure CGI script, serving single request
+ exit;
+}
+
+## ======================================================================
+## action links
+
+# possible values of extra options
+# -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
+# -replay => 1 - start from a current view (replay with modifications)
+# -path_info => 0|1 - don't use/use path_info URL (if possible)
+# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
+sub href {
+ my %params = @_;
+ # default is to use -absolute url() i.e. $my_uri
+ my $href = $params{-full} ? $my_url : $my_uri;
+
+ # implicit -replay, must be first of implicit params
+ $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
+
+ $params{'project'} = $project unless exists $params{'project'};
+
+ if ($params{-replay}) {
+ while (my ($name, $symbol) = each %cgi_param_mapping) {
+ if (!exists $params{$name}) {
+ $params{$name} = $input_params{$name};
+ }
+ }
+ }
+
+ my $use_pathinfo = gitweb_check_feature('pathinfo');
+ if (defined $params{'project'} &&
+ (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
+ # try to put as many parameters as possible in PATH_INFO:
+ # - project name
+ # - action
+ # - hash_parent or hash_parent_base:/file_parent
+ # - hash or hash_base:/filename
+ # - the snapshot_format as an appropriate suffix
+
+ # When the script is the root DirectoryIndex for the domain,
+ # $href here would be something like http://gitweb.example.com/
+ # Thus, we strip any trailing / from $href, to spare us double
+ # slashes in the final URL
+ $href =~ s,/$,,;
+
+ # Then add the project name, if present
+ $href .= "/".esc_path_info($params{'project'});
+ delete $params{'project'};
+
+ # since we destructively absorb parameters, we keep this
+ # boolean that remembers if we're handling a snapshot
+ my $is_snapshot = $params{'action'} eq 'snapshot';
+
+ # Summary just uses the project path URL, any other action is
+ # added to the URL
+ if (defined $params{'action'}) {
+ $href .= "/".esc_path_info($params{'action'})
+ unless $params{'action'} eq 'summary';
+ delete $params{'action'};
+ }
+
+ # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
+ # stripping nonexistent or useless pieces
+ $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
+ || $params{'hash_parent'} || $params{'hash'});
+ if (defined $params{'hash_base'}) {
+ if (defined $params{'hash_parent_base'}) {
+ $href .= esc_path_info($params{'hash_parent_base'});
+ # skip the file_parent if it's the same as the file_name
+ if (defined $params{'file_parent'}) {
+ if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
+ delete $params{'file_parent'};
+ } elsif ($params{'file_parent'} !~ /\.\./) {
+ $href .= ":/".esc_path_info($params{'file_parent'});
+ delete $params{'file_parent'};
+ }
+ }
+ $href .= "..";
+ delete $params{'hash_parent'};
+ delete $params{'hash_parent_base'};
+ } elsif (defined $params{'hash_parent'}) {
+ $href .= esc_path_info($params{'hash_parent'}). "..";
+ delete $params{'hash_parent'};
+ }
+
+ $href .= esc_path_info($params{'hash_base'});
+ if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
+ $href .= ":/".esc_path_info($params{'file_name'});
+ delete $params{'file_name'};
+ }
+ delete $params{'hash'};
+ delete $params{'hash_base'};
+ } elsif (defined $params{'hash'}) {
+ $href .= esc_path_info($params{'hash'});
+ delete $params{'hash'};
+ }
+
+ # If the action was a snapshot, we can absorb the
+ # snapshot_format parameter too
+ if ($is_snapshot) {
+ my $fmt = $params{'snapshot_format'};
+ # snapshot_format should always be defined when href()
+ # is called, but just in case some code forgets, we
+ # fall back to the default
+ $fmt ||= $snapshot_fmts[0];
+ $href .= $known_snapshot_formats{$fmt}{'suffix'};
+ delete $params{'snapshot_format'};
+ }
+ }
+
+ # now encode the parameters explicitly
+ my @result = ();
+ for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
+ my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
+ if (defined $params{$name}) {
+ if (ref($params{$name}) eq "ARRAY") {
+ foreach my $par (@{$params{$name}}) {
+ push @result, $symbol . "=" . esc_param($par);
+ }
+ } else {
+ push @result, $symbol . "=" . esc_param($params{$name});
+ }
+ }
+ }
+ $href .= "?" . join(';', @result) if scalar @result;
+
+ # final transformation: trailing spaces must be escaped (URI-encoded)
+ $href =~ s/(\s+)$/CGI::escape($1)/e;
+
+ if ($params{-anchor}) {
+ $href .= "#".esc_param($params{-anchor});
+ }
+
+ return $href;
+}
+
+
+## ======================================================================
+## validation, quoting/unquoting and escaping
+
+sub is_valid_action {
+ my $input = shift;
+ return undef unless exists $actions{$input};
+ return 1;
+}
+
+sub is_valid_project {
+ my $input = shift;
+
+ return unless defined $input;
+ if (!is_valid_pathname($input) ||
+ !(-d "$projectroot/$input") ||
+ !check_export_ok("$projectroot/$input") ||
+ ($strict_export && !project_in_list($input))) {
+ return undef;
+ } else {
+ return 1;
+ }
+}
+
+sub is_valid_pathname {
+ my $input = shift;
+
+ return undef unless defined $input;
+ # no '.' or '..' as elements of path, i.e. no '.' or '..'
+ # at the beginning, at the end, and between slashes.
+ # also this catches doubled slashes
+ if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
+ return undef;
+ }
+ # no null characters
+ if ($input =~ m!\0!) {
+ return undef;
+ }
+ return 1;
+}
+
+sub is_valid_ref_format {
+ my $input = shift;
+
+ return undef unless defined $input;
+ # restrictions on ref name according to git-check-ref-format
+ if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
+ return undef;
+ }
+ return 1;
+}
+
+sub is_valid_refname {
+ my $input = shift;
+
+ return undef unless defined $input;
+ # textual hashes are O.K.
+ if ($input =~ m/^$oid_regex$/) {
+ return 1;
+ }
+ # it must be correct pathname
+ is_valid_pathname($input) or return undef;
+ # check git-check-ref-format restrictions
+ is_valid_ref_format($input) or return undef;
+ return 1;
+}
+
+# decode sequences of octets in utf8 into Perl's internal form,
+# which is utf-8 with utf8 flag set if needed. gitweb writes out
+# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
+sub to_utf8 {
+ my $str = shift;
+ return undef unless defined $str;
+
+ if (utf8::is_utf8($str) || utf8::decode($str)) {
+ return $str;
+ } else {
+ return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
+ }
+}
+
+# quote unsafe chars, but keep the slash, even when it's not
+# correct, but quoted slashes look too horrible in bookmarks
+sub esc_param {
+ my $str = shift;
+ return undef unless defined $str;
+ $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
+ $str =~ s/ /\+/g;
+ return $str;
+}
+
+# the quoting rules for path_info fragment are slightly different
+sub esc_path_info {
+ my $str = shift;
+ return undef unless defined $str;
+
+ # path_info doesn't treat '+' as space (specially), but '?' must be escaped
+ $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
+
+ return $str;
+}
+
+# quote unsafe chars in whole URL, so some characters cannot be quoted
+sub esc_url {
+ my $str = shift;
+ return undef unless defined $str;
+ $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
+ $str =~ s/ /\+/g;
+ return $str;
+}
+
+# quote unsafe characters in HTML attributes
+sub esc_attr {
+
+ # for XHTML conformance escaping '"' to '"' is not enough
+ return esc_html(@_);
+}
+
+# replace invalid utf8 character with SUBSTITUTION sequence
+sub esc_html {
+ my $str = shift;
+ my %opts = @_;
+
+ return undef unless defined $str;
+
+ $str = to_utf8($str);
+ $str = $cgi->escapeHTML($str);
+ if ($opts{'-nbsp'}) {
+ $str =~ s/ / /g;
+ }
+ $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
+ return $str;
+}
+
+# quote control characters and escape filename to HTML
+sub esc_path {
+ my $str = shift;
+ my %opts = @_;
+
+ return undef unless defined $str;
+
+ $str = to_utf8($str);
+ $str = $cgi->escapeHTML($str);
+ if ($opts{'-nbsp'}) {
+ $str =~ s/ / /g;
+ }
+ $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
+ return $str;
+}
+
+# Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0)
+sub sanitize {
+ my $str = shift;
+
+ return undef unless defined $str;
+
+ $str = to_utf8($str);
+ $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
+ return $str;
+}
+
+# Make control characters "printable", using character escape codes (CEC)
+sub quot_cec {
+ my $cntrl = shift;
+ my %opts = @_;
+ my %es = ( # character escape codes, aka escape sequences
+ "\t" => '\t', # tab (HT)
+ "\n" => '\n', # line feed (LF)
+ "\r" => '\r', # carriage return (CR)
+ "\f" => '\f', # form feed (FF)
+ "\b" => '\b', # backspace (BS)
+ "\a" => '\a', # alarm (bell) (BEL)
+ "\e" => '\e', # escape (ESC)
+ "\013" => '\v', # vertical tab (VT)
+ "\000" => '\0', # nul character (NUL)
+ );
+ my $chr = ( (exists $es{$cntrl})
+ ? $es{$cntrl}
+ : sprintf('\%2x', ord($cntrl)) );
+ if ($opts{-nohtml}) {
+ return $chr;
+ } else {
+ return "$chr";
+ }
+}
+
+# Alternatively use unicode control pictures codepoints,
+# Unicode "printable representation" (PR)
+sub quot_upr {
+ my $cntrl = shift;
+ my %opts = @_;
+
+ my $chr = sprintf('%04d;', 0x2400+ord($cntrl));
+ if ($opts{-nohtml}) {
+ return $chr;
+ } else {
+ return "$chr";
+ }
+}
+
+# git may return quoted and escaped filenames
+sub unquote {
+ my $str = shift;
+
+ sub unq {
+ my $seq = shift;
+ my %es = ( # character escape codes, aka escape sequences
+ 't' => "\t", # tab (HT, TAB)
+ 'n' => "\n", # newline (NL)
+ 'r' => "\r", # return (CR)
+ 'f' => "\f", # form feed (FF)
+ 'b' => "\b", # backspace (BS)
+ 'a' => "\a", # alarm (bell) (BEL)
+ 'e' => "\e", # escape (ESC)
+ 'v' => "\013", # vertical tab (VT)
+ );
+
+ if ($seq =~ m/^[0-7]{1,3}$/) {
+ # octal char sequence
+ return chr(oct($seq));
+ } elsif (exists $es{$seq}) {
+ # C escape sequence, aka character escape code
+ return $es{$seq};
+ }
+ # quoted ordinary character
+ return $seq;
+ }
+
+ if ($str =~ m/^"(.*)"$/) {
+ # needs unquoting
+ $str = $1;
+ $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
+ }
+ return $str;
+}
+
+# escape tabs (convert tabs to spaces)
+sub untabify {
+ my $line = shift;
+
+ while ((my $pos = index($line, "\t")) != -1) {
+ if (my $count = (8 - ($pos % 8))) {
+ my $spaces = ' ' x $count;
+ $line =~ s/\t/$spaces/;
+ }
+ }
+
+ return $line;
+}
+
+sub project_in_list {
+ my $project = shift;
+ my @list = git_get_projects_list();
+ return @list && scalar(grep { $_->{'path'} eq $project } @list);
+}
+
+## ----------------------------------------------------------------------
+## HTML aware string manipulation
+
+# Try to chop given string on a word boundary between position
+# $len and $len+$add_len. If there is no word boundary there,
+# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
+# (marking chopped part) would be longer than given string.
+sub chop_str {
+ my $str = shift;
+ my $len = shift;
+ my $add_len = shift || 10;
+ my $where = shift || 'right'; # 'left' | 'center' | 'right'
+
+ # Make sure perl knows it is utf8 encoded so we don't
+ # cut in the middle of a utf8 multibyte char.
+ $str = to_utf8($str);
+
+ # allow only $len chars, but don't cut a word if it would fit in $add_len
+ # if it doesn't fit, cut it if it's still longer than the dots we would add
+ # remove chopped character entities entirely
+
+ # when chopping in the middle, distribute $len into left and right part
+ # return early if chopping wouldn't make string shorter
+ if ($where eq 'center') {
+ return $str if ($len + 5 >= length($str)); # filler is length 5
+ $len = int($len/2);
+ } else {
+ return $str if ($len + 4 >= length($str)); # filler is length 4
+ }
+
+ # regexps: ending and beginning with word part up to $add_len
+ my $endre = qr/.{$len}\w{0,$add_len}/;
+ my $begre = qr/\w{0,$add_len}.{$len}/;
+
+ if ($where eq 'left') {
+ $str =~ m/^(.*?)($begre)$/;
+ my ($lead, $body) = ($1, $2);
+ if (length($lead) > 4) {
+ $lead = " ...";
+ }
+ return "$lead$body";
+
+ } elsif ($where eq 'center') {
+ $str =~ m/^($endre)(.*)$/;
+ my ($left, $str) = ($1, $2);
+ $str =~ m/^(.*?)($begre)$/;
+ my ($mid, $right) = ($1, $2);
+ if (length($mid) > 5) {
+ $mid = " ... ";
+ }
+ return "$left$mid$right";
+
+ } else {
+ $str =~ m/^($endre)(.*)$/;
+ my $body = $1;
+ my $tail = $2;
+ if (length($tail) > 4) {
+ $tail = "... ";
+ }
+ return "$body$tail";
+ }
+}
+
+# takes the same arguments as chop_str, but also wraps a around the
+# result with a title attribute if it does get chopped. Additionally, the
+# string is HTML-escaped.
+sub chop_and_escape_str {
+ my ($str) = @_;
+
+ my $chopped = chop_str(@_);
+ $str = to_utf8($str);
+ if ($chopped eq $str) {
+ return esc_html($chopped);
+ } else {
+ $str =~ s/[[:cntrl:]]/?/g;
+ return $cgi->span({-title=>$str}, esc_html($chopped));
+ }
+}
+
+# Highlight selected fragments of string, using given CSS class,
+# and escape HTML. It is assumed that fragments do not overlap.
+# Regions are passed as list of pairs (array references).
+#
+# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
+# 'foobar'
+sub esc_html_hl_regions {
+ my ($str, $css_class, @sel) = @_;
+ my %opts = grep { ref($_) ne 'ARRAY' } @sel;
+ @sel = grep { ref($_) eq 'ARRAY' } @sel;
+ return esc_html($str, %opts) unless @sel;
+
+ my $out = '';
+ my $pos = 0;
+
+ for my $s (@sel) {
+ my ($begin, $end) = @$s;
+
+ # Don't create empty elements.
+ next if $end <= $begin;
+
+ my $escaped = esc_html(substr($str, $begin, $end - $begin),
+ %opts);
+
+ $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
+ if ($begin - $pos > 0);
+ $out .= $cgi->span({-class => $css_class}, $escaped);
+
+ $pos = $end;
+ }
+ $out .= esc_html(substr($str, $pos), %opts)
+ if ($pos < length($str));
+
+ return $out;
+}
+
+# return positions of beginning and end of each match
+sub matchpos_list {
+ my ($str, $regexp) = @_;
+ return unless (defined $str && defined $regexp);
+
+ my @matches;
+ while ($str =~ /$regexp/g) {
+ push @matches, [$-[0], $+[0]];
+ }
+ return @matches;
+}
+
+# highlight match (if any), and escape HTML
+sub esc_html_match_hl {
+ my ($str, $regexp) = @_;
+ return esc_html($str) unless defined $regexp;
+
+ my @matches = matchpos_list($str, $regexp);
+ return esc_html($str) unless @matches;
+
+ return esc_html_hl_regions($str, 'match', @matches);
+}
+
+
+# highlight match (if any) of shortened string, and escape HTML
+sub esc_html_match_hl_chopped {
+ my ($str, $chopped, $regexp) = @_;
+ return esc_html_match_hl($str, $regexp) unless defined $chopped;
+
+ my @matches = matchpos_list($str, $regexp);
+ return esc_html($chopped) unless @matches;
+
+ # filter matches so that we mark chopped string
+ my $tail = "... "; # see chop_str
+ unless ($chopped =~ s/\Q$tail\E$//) {
+ $tail = '';
+ }
+ my $chop_len = length($chopped);
+ my $tail_len = length($tail);
+ my @filtered;
+
+ for my $m (@matches) {
+ if ($m->[0] > $chop_len) {
+ push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
+ last;
+ } elsif ($m->[1] > $chop_len) {
+ push @filtered, [ $m->[0], $chop_len + $tail_len ];
+ last;
+ }
+ push @filtered, $m;
+ }
+
+ return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
+}
+
+## ----------------------------------------------------------------------
+## functions returning short strings
+
+# CSS class for given age value (in seconds)
+sub age_class {
+ my $age = shift;
+
+ if (!defined $age) {
+ return "noage";
+ } elsif ($age < 60*60*2) {
+ return "age0";
+ } elsif ($age < 60*60*24*2) {
+ return "age1";
+ } else {
+ return "age2";
+ }
+}
+
+# convert age in seconds to "nn units ago" string
+sub age_string {
+ my $age = shift;
+ my $age_str;
+
+ if ($age > 60*60*24*365*2) {
+ $age_str = (int $age/60/60/24/365);
+ $age_str .= " years ago";
+ } elsif ($age > 60*60*24*(365/12)*2) {
+ $age_str = int $age/60/60/24/(365/12);
+ $age_str .= " months ago";
+ } elsif ($age > 60*60*24*7*2) {
+ $age_str = int $age/60/60/24/7;
+ $age_str .= " weeks ago";
+ } elsif ($age > 60*60*24*2) {
+ $age_str = int $age/60/60/24;
+ $age_str .= " days ago";
+ } elsif ($age > 60*60*2) {
+ $age_str = int $age/60/60;
+ $age_str .= " hours ago";
+ } elsif ($age > 60*2) {
+ $age_str = int $age/60;
+ $age_str .= " min ago";
+ } elsif ($age > 2) {
+ $age_str = int $age;
+ $age_str .= " sec ago";
+ } else {
+ $age_str .= " right now";
+ }
+ return $age_str;
+}
+
+use constant {
+ S_IFINVALID => 0030000,
+ S_IFGITLINK => 0160000,
+};
+
+# submodule/subproject, a commit object reference
+sub S_ISGITLINK {
+ my $mode = shift;
+
+ return (($mode & S_IFMT) == S_IFGITLINK)
+}
+
+# convert file mode in octal to symbolic file mode string
+sub mode_str {
+ my $mode = oct shift;
+
+ if (S_ISGITLINK($mode)) {
+ return 'm---------';
+ } elsif (S_ISDIR($mode & S_IFMT)) {
+ return 'drwxr-xr-x';
+ } elsif (S_ISLNK($mode)) {
+ return 'lrwxrwxrwx';
+ } elsif (S_ISREG($mode)) {
+ # git cares only about the executable bit
+ if ($mode & S_IXUSR) {
+ return '-rwxr-xr-x';
+ } else {
+ return '-rw-r--r--';
+ };
+ } else {
+ return '----------';
+ }
+}
+
+# convert file mode in octal to file type string
+sub file_type {
+ my $mode = shift;
+
+ if ($mode !~ m/^[0-7]+$/) {
+ return $mode;
+ } else {
+ $mode = oct $mode;
+ }
+
+ if (S_ISGITLINK($mode)) {
+ return "submodule";
+ } elsif (S_ISDIR($mode & S_IFMT)) {
+ return "directory";
+ } elsif (S_ISLNK($mode)) {
+ return "symlink";
+ } elsif (S_ISREG($mode)) {
+ return "file";
+ } else {
+ return "unknown";
+ }
+}
+
+# convert file mode in octal to file type description string
+sub file_type_long {
+ my $mode = shift;
+
+ if ($mode !~ m/^[0-7]+$/) {
+ return $mode;
+ } else {
+ $mode = oct $mode;
+ }
+
+ if (S_ISGITLINK($mode)) {
+ return "submodule";
+ } elsif (S_ISDIR($mode & S_IFMT)) {
+ return "directory";
+ } elsif (S_ISLNK($mode)) {
+ return "symlink";
+ } elsif (S_ISREG($mode)) {
+ if ($mode & S_IXUSR) {
+ return "executable";
+ } else {
+ return "file";
+ };
+ } else {
+ return "unknown";
+ }
+}
+
+
+## ----------------------------------------------------------------------
+## functions returning short HTML fragments, or transforming HTML fragments
+## which don't belong to other sections
+
+# format line of commit message.
+sub format_log_line_html {
+ my $line = shift;
+
+ # Potentially abbreviated OID.
+ my $regex = oid_nlen_regex("7,64");
+
+ $line = esc_html($line, -nbsp=>1);
+ $line =~ s{
+ \b
+ (
+ # The output of "git describe", e.g. v2.10.0-297-gf6727b0
+ # or hadoop-20160921-113441-20-g094fb7d
+ (?a({-href => href(action=>"object", hash=>$1),
+ -class => "text"}, $1);
+ }egx;
+
+ return $line;
+}
+
+# format marker of refs pointing to given object
+
+# the destination action is chosen based on object type and current context:
+# - for annotated tags, we choose the tag view unless it's the current view
+# already, in which case we go to shortlog view
+# - for other refs, we keep the current view if we're in history, shortlog or
+# log view, and select shortlog otherwise
+sub format_ref_marker {
+ my ($refs, $id) = @_;
+ my $markers = '';
+
+ if (defined $refs->{$id}) {
+ foreach my $ref (@{$refs->{$id}}) {
+ # this code exploits the fact that non-lightweight tags are the
+ # only indirect objects, and that they are the only objects for which
+ # we want to use tag instead of shortlog as action
+ my ($type, $name) = qw();
+ my $indirect = ($ref =~ s/\^\{\}$//);
+ # e.g. tags/v2.6.11 or heads/next
+ if ($ref =~ m!^(.*?)s?/(.*)$!) {
+ $type = $1;
+ $name = $2;
+ } else {
+ $type = "ref";
+ $name = $ref;
+ }
+
+ my $class = $type;
+ $class .= " indirect" if $indirect;
+
+ my $dest_action = "shortlog";
+
+ if ($indirect) {
+ $dest_action = "tag" unless $action eq "tag";
+ } elsif ($action =~ /^(history|(short)?log)$/) {
+ $dest_action = $action;
+ }
+
+ my $dest = "";
+ $dest .= "refs/" unless $ref =~ m!^refs/!;
+ $dest .= $ref;
+
+ my $link = $cgi->a({
+ -href => href(
+ action=>$dest_action,
+ hash=>$dest
+ )}, esc_html($name));
+
+ $markers .= " " .
+ $link . "";
+ }
+ }
+
+ if ($markers) {
+ return ' '. $markers . '';
+ } else {
+ return "";
+ }
+}
+
+# format, perhaps shortened and with markers, title line
+sub format_subject_html {
+ my ($long, $short, $href, $extra) = @_;
+ $extra = '' unless defined($extra);
+
+ if (length($short) < length($long)) {
+ $long =~ s/[[:cntrl:]]/?/g;
+ return $cgi->a({-href => $href, -class => "list subject",
+ -title => to_utf8($long)},
+ esc_html($short)) . $extra;
+ } else {
+ return $cgi->a({-href => $href, -class => "list subject"},
+ esc_html($long)) . $extra;
+ }
+}
+
+# Rather than recomputing the url for an email multiple times, we cache it
+# after the first hit. This gives a visible benefit in views where the avatar
+# for the same email is used repeatedly (e.g. shortlog).
+# The cache is shared by all avatar engines (currently gravatar only), which
+# are free to use it as preferred. Since only one avatar engine is used for any
+# given page, there's no risk for cache conflicts.
+our %avatar_cache = ();
+
+# Compute the picon url for a given email, by using the picon search service over at
+# http://www.cs.indiana.edu/picons/search.html
+sub picon_url {
+ my $email = lc shift;
+ if (!$avatar_cache{$email}) {
+ my ($user, $domain) = split('@', $email);
+ $avatar_cache{$email} =
+ "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
+ "$domain/$user/" .
+ "users+domains+unknown/up/single";
+ }
+ return $avatar_cache{$email};
+}
+
+# Compute the gravatar url for a given email, if it's not in the cache already.
+# Gravatar stores only the part of the URL before the size, since that's the
+# one computationally more expensive. This also allows reuse of the cache for
+# different sizes (for this particular engine).
+sub gravatar_url {
+ my $email = lc shift;
+ my $size = shift;
+ $avatar_cache{$email} ||=
+ "//www.gravatar.com/avatar/" .
+ md5_hex($email) . "?s=";
+ return $avatar_cache{$email} . $size;
+}
+
+# Insert an avatar for the given $email at the given $size if the feature
+# is enabled.
+sub git_get_avatar {
+ my ($email, %opts) = @_;
+ my $pre_white = ($opts{-pad_before} ? " " : "");
+ my $post_white = ($opts{-pad_after} ? " " : "");
+ $opts{-size} ||= 'default';
+ my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
+ my $url = "";
+ if ($git_avatar eq 'gravatar') {
+ $url = gravatar_url($email, $size);
+ } elsif ($git_avatar eq 'picon') {
+ $url = picon_url($email);
+ }
+ # Other providers can be added by extending the if chain, defining $url
+ # as needed. If no variant puts something in $url, we assume avatars
+ # are completely disabled/unavailable.
+ if ($url) {
+ return $pre_white .
+ "" . $post_white;
+ } else {
+ return "";
+ }
+}
+
+sub format_search_author {
+ my ($author, $searchtype, $displaytext) = @_;
+ my $have_search = gitweb_check_feature('search');
+
+ if ($have_search) {
+ my $performed = "";
+ if ($searchtype eq 'author') {
+ $performed = "authored";
+ } elsif ($searchtype eq 'committer') {
+ $performed = "committed";
+ }
+
+ return $cgi->a({-href => href(action=>"search", hash=>$hash,
+ searchtext=>$author,
+ searchtype=>$searchtype), class=>"list",
+ title=>"Search for commits $performed by $author"},
+ $displaytext);
+
+ } else {
+ return $displaytext;
+ }
+}
+
+# format the author name of the given commit with the given tag
+# the author name is chopped and escaped according to the other
+# optional parameters (see chop_str).
+sub format_author_html {
+ my $tag = shift;
+ my $co = shift;
+ my $author = chop_and_escape_str($co->{'author_name'}, @_);
+ return "<$tag class=\"author\">" .
+ format_search_author($co->{'author_name'}, "author",
+ git_get_avatar($co->{'author_email'}, -pad_after => 1) .
+ $author) .
+ "$tag>";
+}
+
+# format git diff header line, i.e. "diff --(git|combined|cc) ..."
+sub format_git_diff_header_line {
+ my $line = shift;
+ my $diffinfo = shift;
+ my ($from, $to) = @_;
+
+ if ($diffinfo->{'nparents'}) {
+ # combined diff
+ $line =~ s!^(diff (.*?) )"?.*$!$1!;
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+ esc_path($to->{'file'}));
+ } else { # file was deleted (no href)
+ $line .= esc_path($to->{'file'});
+ }
+ } else {
+ # "ordinary" diff
+ $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
+ if ($from->{'href'}) {
+ $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
+ 'a/' . esc_path($from->{'file'}));
+ } else { # file was added (no href)
+ $line .= 'a/' . esc_path($from->{'file'});
+ }
+ $line .= ' ';
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
+ 'b/' . esc_path($to->{'file'}));
+ } else { # file was deleted
+ $line .= 'b/' . esc_path($to->{'file'});
+ }
+ }
+
+ return "
$line
\n";
+}
+
+# format extended diff header line, before patch itself
+sub format_extended_diff_header_line {
+ my $line = shift;
+ my $diffinfo = shift;
+ my ($from, $to) = @_;
+
+ # match
+ if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
+ $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+ esc_path($from->{'file'}));
+ }
+ if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
+ $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+ esc_path($to->{'file'}));
+ }
+ # match single
+ if ($line =~ m/\s(\d{6})$/) {
+ $line .= ' (' .
+ file_type_long($1) .
+ ')';
+ }
+ # match
+ if ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", ",") |
+ $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", ",")) {
+ # can match only for combined diff
+ $line = 'index ';
+ for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+ if ($from->{'href'}[$i]) {
+ $line .= $cgi->a({-href=>$from->{'href'}[$i],
+ -class=>"hash"},
+ substr($diffinfo->{'from_id'}[$i],0,7));
+ } else {
+ $line .= '0' x 7;
+ }
+ # separator
+ $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
+ }
+ $line .= '..';
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'to_id'},0,7));
+ } else {
+ $line .= '0' x 7;
+ }
+
+ } elsif ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", "..") |
+ $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", "..")) {
+ # can match only for ordinary diff
+ my ($from_link, $to_link);
+ if ($from->{'href'}) {
+ $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'from_id'},0,7));
+ } else {
+ $from_link = '0' x 7;
+ }
+ if ($to->{'href'}) {
+ $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'to_id'},0,7));
+ } else {
+ $to_link = '0' x 7;
+ }
+ my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
+ $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
+ }
+
+ return $line . " \n";
+}
+
+# format from-file/to-file diff header
+sub format_diff_from_to_header {
+ my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
+ my $line;
+ my $result = '';
+
+ $line = $from_line;
+ #assert($line =~ m/^---/) if DEBUG;
+ # no extra formatting for "^--- /dev/null"
+ if (! $diffinfo->{'nparents'}) {
+ # ordinary (single parent) diff
+ if ($line =~ m!^--- "?a/!) {
+ if ($from->{'href'}) {
+ $line = '--- a/' .
+ $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+ esc_path($from->{'file'}));
+ } else {
+ $line = '--- a/' .
+ esc_path($from->{'file'});
+ }
+ }
+ $result .= qq!
\n"; # class="page_footer"
+ }
+
+ if (defined $site_footer && -f $site_footer) {
+ insert_file($site_footer);
+ }
+
+ print qq!\n!;
+ if (defined $action &&
+ $action eq 'blame_incremental') {
+ print qq!\n!;
+ } else {
+ my ($jstimezone, $tz_cookie, $datetime_class) =
+ gitweb_get_feature('javascript-timezone');
+
+ print qq!\n!;
+ }
+
+ print "\n" .
+ "";
+}
+
+# die_error(, [, ])
+# Example: die_error(404, 'Hash not found')
+# By convention, use the following status codes (as defined in RFC 2616):
+# 400: Invalid or missing CGI parameters, or
+# requested object exists but has wrong type.
+# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
+# this server or project.
+# 404: Requested object/revision/project doesn't exist.
+# 500: The server isn't configured properly, or
+# an internal error occurred (e.g. failed assertions caused by bugs), or
+# an unknown error occurred (e.g. the git binary died unexpectedly).
+# 503: The server is currently unavailable (because it is overloaded,
+# or down for maintenance). Generally, this is a temporary state.
+sub die_error {
+ my $status = shift || 500;
+ my $error = esc_html(shift) || "Internal Server Error";
+ my $extra = shift;
+ my %opts = @_;
+
+ my %http_responses = (
+ 400 => '400 Bad Request',
+ 403 => '403 Forbidden',
+ 404 => '404 Not Found',
+ 500 => '500 Internal Server Error',
+ 503 => '503 Service Unavailable',
+ );
+ git_header_html($http_responses{$status}, undef, %opts);
+ print <
+
\n";
+}
+
+# Group output by placing it in a DIV element and adding a header.
+# Options for start_div() can be provided by passing a hash reference as the
+# first parameter to the function.
+# Options to git_print_header_div() can be provided by passing an array
+# reference. This must follow the options to start_div if they are present.
+# The content can be a scalar, which is output as-is, a scalar reference, which
+# is output after html escaping, an IO handle passed either as *handle or
+# *handle{IO}, or a function reference. In the latter case all following
+# parameters will be taken as argument to the content function call.
+sub git_print_section {
+ my ($div_args, $header_args, $content);
+ my $arg = shift;
+ if (ref($arg) eq 'HASH') {
+ $div_args = $arg;
+ $arg = shift;
+ }
+ if (ref($arg) eq 'ARRAY') {
+ $header_args = $arg;
+ $arg = shift;
+ }
+ $content = $arg;
+
+ print $cgi->start_div($div_args);
+ git_print_header_div(@$header_args);
+
+ if (ref($content) eq 'CODE') {
+ $content->(@_);
+ } elsif (ref($content) eq 'SCALAR') {
+ print esc_html($$content);
+ } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
+ print <$content>;
+ } elsif (!ref($content) && defined($content)) {
+ print $content;
+ }
+
+ print $cgi->end_div;
+}
+
+sub format_timestamp_html {
+ my $date = shift;
+ my $strtime = $date->{'rfc2822'};
+
+ my (undef, undef, $datetime_class) =
+ gitweb_get_feature('javascript-timezone');
+ if ($datetime_class) {
+ $strtime = qq!$strtime!;
+ }
+
+ my $localtime_format = '(%02d:%02d %s)';
+ if ($date->{'hour_local'} < 6) {
+ $localtime_format = '(%02d:%02d %s)';
+ }
+ $strtime .= ' ' .
+ sprintf($localtime_format,
+ $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
+
+ return $strtime;
+}
+
+# Outputs the author name and date in long form
+sub git_print_authorship {
+ my $co = shift;
+ my %opts = @_;
+ my $tag = $opts{-tag} || 'div';
+ my $author = $co->{'author_name'};
+
+ my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
+ print "<$tag class=\"author_date\">" .
+ format_search_author($author, "author", esc_html($author)) .
+ " [".format_timestamp_html(\%ad)."]".
+ git_get_avatar($co->{'author_email'}, -pad_before => 1) .
+ "$tag>\n";
+}
+
+# Outputs table rows containing the full author or committer information,
+# in the format expected for 'commit' view (& similar).
+# Parameters are a commit hash reference, followed by the list of people
+# to output information for. If the list is empty it defaults to both
+# author and committer.
+sub git_print_authorship_rows {
+ my $co = shift;
+ # too bad we can't use @people = @_ || ('author', 'committer')
+ my @people = @_;
+ @people = ('author', 'committer') unless @people;
+ foreach my $who (@people) {
+ my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
+ print "
\n";
+}
+
+sub git_print_log {
+ my $log = shift;
+ my %opts = @_;
+
+ if ($opts{'-remove_title'}) {
+ # remove title, i.e. first line of log
+ shift @$log;
+ }
+ # remove leading empty lines
+ while (defined $log->[0] && $log->[0] eq "") {
+ shift @$log;
+ }
+
+ # print log
+ my $skip_blank_line = 0;
+ foreach my $line (@$log) {
+ if ($line =~ m/^\s*([A-Z][-A-Za-z]*-([Bb]y|[Tt]o)|C[Cc]|(Clos|Fix)es): /) {
+ if (! $opts{'-remove_signoff'}) {
+ print "" . esc_html($line) . " \n";
+ $skip_blank_line = 1;
+ }
+ next;
+ }
+
+ if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
+ if (! $opts{'-remove_signoff'}) {
+ print "" . esc_html($1) . ": " .
+ "" . esc_html($2) . "" .
+ " \n";
+ $skip_blank_line = 1;
+ }
+ next;
+ }
+
+ # print only one empty line
+ # do not print empty line after signoff
+ if ($line eq "") {
+ next if ($skip_blank_line);
+ $skip_blank_line = 1;
+ } else {
+ $skip_blank_line = 0;
+ }
+
+ print format_log_line_html($line) . " \n";
+ }
+
+ if ($opts{'-final_empty_line'}) {
+ # end with single empty line
+ print " \n" unless $skip_blank_line;
+ }
+}
+
+# return link target (what link points to)
+sub git_get_link_target {
+ my $hash = shift;
+ my $link_target;
+
+ # read link
+ open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
+ or return;
+ {
+ local $/ = undef;
+ $link_target = <$fd>;
+ }
+ close $fd
+ or return;
+
+ return $link_target;
+}
+
+# given link target, and the directory (basedir) the link is in,
+# return target of link relative to top directory (top tree);
+# return undef if it is not possible (including absolute links).
+sub normalize_link_target {
+ my ($link_target, $basedir) = @_;
+
+ # absolute symlinks (beginning with '/') cannot be normalized
+ return if (substr($link_target, 0, 1) eq '/');
+
+ # normalize link target to path from top (root) tree (dir)
+ my $path;
+ if ($basedir) {
+ $path = $basedir . '/' . $link_target;
+ } else {
+ # we are in top (root) tree (dir)
+ $path = $link_target;
+ }
+
+ # remove //, /./, and /../
+ my @path_parts;
+ foreach my $part (split('/', $path)) {
+ # discard '.' and ''
+ next if (!$part || $part eq '.');
+ # handle '..'
+ if ($part eq '..') {
+ if (@path_parts) {
+ pop @path_parts;
+ } else {
+ # link leads outside repository (outside top dir)
+ return;
+ }
+ } else {
+ push @path_parts, $part;
+ }
+ }
+ $path = join('/', @path_parts);
+
+ return $path;
+}
+
+# print tree entry (row of git_tree), but without encompassing
element
+sub git_print_tree_entry {
+ my ($t, $basedir, $hash_base, $have_blame) = @_;
+
+ my %base_key = ();
+ $base_key{'hash_base'} = $hash_base if defined $hash_base;
+
+ # The format of a table row is: mode list link. Where mode is
+ # the mode of the entry, list is the name of the entry, an href,
+ # and link is the action links of the entry.
+
+ print "
';
+ }
+}
+
+# Print context lines and then rem/add lines in inline manner.
+sub print_inline_diff_lines {
+ my ($ctx, $rem, $add) = @_;
+
+ print @$ctx, @$rem, @$add;
+}
+
+# Format removed and added line, mark changed part and HTML-format them.
+# Implementation is based on contrib/diff-highlight
+sub format_rem_add_lines_pair {
+ my ($rem, $add, $num_parents) = @_;
+
+ # We need to untabify lines before split()'ing them;
+ # otherwise offsets would be invalid.
+ chomp $rem;
+ chomp $add;
+ $rem = untabify($rem);
+ $add = untabify($add);
+
+ my @rem = split(//, $rem);
+ my @add = split(//, $add);
+ my ($esc_rem, $esc_add);
+ # Ignore leading +/- characters for each parent.
+ my ($prefix_len, $suffix_len) = ($num_parents, 0);
+ my ($prefix_has_nonspace, $suffix_has_nonspace);
+
+ my $shorter = (@rem < @add) ? @rem : @add;
+ while ($prefix_len < $shorter) {
+ last if ($rem[$prefix_len] ne $add[$prefix_len]);
+
+ $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
+ $prefix_len++;
+ }
+
+ while ($prefix_len + $suffix_len < $shorter) {
+ last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
+
+ $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
+ $suffix_len++;
+ }
+
+ # Mark lines that are different from each other, but have some common
+ # part that isn't whitespace. If lines are completely different, don't
+ # mark them because that would make output unreadable, especially if
+ # diff consists of multiple lines.
+ if ($prefix_has_nonspace || $suffix_has_nonspace) {
+ $esc_rem = esc_html_hl_regions($rem, 'marked',
+ [$prefix_len, @rem - $suffix_len], -nbsp=>1);
+ $esc_add = esc_html_hl_regions($add, 'marked',
+ [$prefix_len, @add - $suffix_len], -nbsp=>1);
+ } else {
+ $esc_rem = esc_html($rem, -nbsp=>1);
+ $esc_add = esc_html($add, -nbsp=>1);
+ }
+
+ return format_diff_line(\$esc_rem, 'rem'),
+ format_diff_line(\$esc_add, 'add');
+}
+
+# HTML-format diff context, removed and added lines.
+sub format_ctx_rem_add_lines {
+ my ($ctx, $rem, $add, $num_parents) = @_;
+ my (@new_ctx, @new_rem, @new_add);
+ my $can_highlight = 0;
+ my $is_combined = ($num_parents > 1);
+
+ # Highlight if every removed line has a corresponding added line.
+ if (@$add > 0 && @$add == @$rem) {
+ $can_highlight = 1;
+
+ # Highlight lines in combined diff only if the chunk contains
+ # diff between the same version, e.g.
+ #
+ # - a
+ # - b
+ # + c
+ # + d
+ #
+ # Otherwise the highlighting would be confusing.
+ if ($is_combined) {
+ for (my $i = 0; $i < @$add; $i++) {
+ my $prefix_rem = substr($rem->[$i], 0, $num_parents);
+ my $prefix_add = substr($add->[$i], 0, $num_parents);
+
+ $prefix_rem =~ s/-/+/g;
+
+ if ($prefix_rem ne $prefix_add) {
+ $can_highlight = 0;
+ last;
+ }
+ }
+ }
+ }
+
+ if ($can_highlight) {
+ for (my $i = 0; $i < @$add; $i++) {
+ my ($line_rem, $line_add) = format_rem_add_lines_pair(
+ $rem->[$i], $add->[$i], $num_parents);
+ push @new_rem, $line_rem;
+ push @new_add, $line_add;
+ }
+ } else {
+ @new_rem = map { format_diff_line($_, 'rem') } @$rem;
+ @new_add = map { format_diff_line($_, 'add') } @$add;
+ }
+
+ @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
+
+ return (\@new_ctx, \@new_rem, \@new_add);
+}
+
+# Print context lines and then rem/add lines.
+sub print_diff_lines {
+ my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
+ my $is_combined = $num_parents > 1;
+
+ ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
+ $num_parents);
+
+ if ($diff_style eq 'sidebyside' && !$is_combined) {
+ print_sidebyside_diff_lines($ctx, $rem, $add);
+ } else {
+ # default 'inline' style and unknown styles
+ print_inline_diff_lines($ctx, $rem, $add);
+ }
+}
+
+sub print_diff_chunk {
+ my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
+ my (@ctx, @rem, @add);
+
+ # The class of the previous line.
+ my $prev_class = '';
+
+ return unless @chunk;
+
+ # incomplete last line might be among removed or added lines,
+ # or both, or among context lines: find which
+ for (my $i = 1; $i < @chunk; $i++) {
+ if ($chunk[$i][0] eq 'incomplete') {
+ $chunk[$i][0] = $chunk[$i-1][0];
+ }
+ }
+
+ # guardian
+ push @chunk, ["", ""];
+
+ foreach my $line_info (@chunk) {
+ my ($class, $line) = @$line_info;
+
+ # print chunk headers
+ if ($class && $class eq 'chunk_header') {
+ print format_diff_line($line, $class, $from, $to);
+ next;
+ }
+
+ ## print from accumulator when have some add/rem lines or end
+ # of chunk (flush context lines), or when have add and rem
+ # lines and new block is reached (otherwise add/rem lines could
+ # be reordered)
+ if (!$class || ((@rem || @add) && $class eq 'ctx') ||
+ (@rem && @add && $class ne $prev_class)) {
+ print_diff_lines(\@ctx, \@rem, \@add,
+ $diff_style, $num_parents);
+ @ctx = @rem = @add = ();
+ }
+
+ ## adding lines to accumulator
+ # guardian value
+ last unless $line;
+ # rem, add or change
+ if ($class eq 'rem') {
+ push @rem, $line;
+ } elsif ($class eq 'add') {
+ push @add, $line;
+ }
+ # context line
+ if ($class eq 'ctx') {
+ push @ctx, $line;
+ }
+
+ $prev_class = $class;
+ }
+}
+
+sub git_patchset_body {
+ my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
+ my ($hash_parent) = $hash_parents[0];
+
+ my $is_combined = (@hash_parents > 1);
+ my $patch_idx = 0;
+ my $patch_number = 0;
+ my $patch_line;
+ my $diffinfo;
+ my $to_name;
+ my (%from, %to);
+ my @chunk; # for side-by-side diff
+
+ print "
\n";
+
+ # skip to first patch
+ while ($patch_line = <$fd>) {
+ chomp $patch_line;
+
+ last if ($patch_line =~ m/^diff /);
+ }
+
+ PATCH:
+ while ($patch_line) {
+
+ # parse "git diff" header line
+ if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
+ # $1 is from_name, which we do not use
+ $to_name = unquote($2);
+ $to_name =~ s!^b/!!;
+ } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
+ # $1 is 'cc' or 'combined', which we do not use
+ $to_name = unquote($2);
+ } else {
+ $to_name = undef;
+ }
+
+ # check if current patch belong to current raw line
+ # and parse raw git-diff line if needed
+ if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
+ # this is continuation of a split patch
+ print "
\n";
+ } else {
+ # advance raw git-diff output if needed
+ $patch_idx++ if defined $diffinfo;
+
+ # read and prepare patch information
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+
+ # compact combined diff output can have some patches skipped
+ # find which patch (using pathname of result) we are at now;
+ if ($is_combined) {
+ while ($to_name ne $diffinfo->{'to_file'}) {
+ print "
\n"; # class="patch"
+
+ $patch_idx++;
+ $patch_number++;
+
+ last if $patch_idx > $#$difftree;
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+ }
+ }
+
+ # modifies %from, %to hashes
+ parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
+
+ # this is first patch for raw difftree line with $patch_idx index
+ # we index @$difftree array from 0, but number patches from 1
+ print "
\n"; # class="patch"
+ }
+
+ # for compact combined (--cc) format, with chunk and patch simplification
+ # the patchset might be empty, but there might be unprocessed raw lines
+ for (++$patch_idx if $patch_number > 0;
+ $patch_idx < @$difftree;
+ ++$patch_idx) {
+ # read and prepare patch information
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
+
+ # generate anchor for "patch" links in difftree / whatchanged part
+ print "
\n";
+ }
+
+ # use per project git URL list in $projectroot/$project/cloneurl
+ # or make project git URL from git base URL and project name
+ my $url_tag = "URL";
+ my @url_list = git_get_project_url_list($project);
+ @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
+ foreach my $git_url (@url_list) {
+ next unless $git_url;
+ print format_repo_url($url_tag, $git_url);
+ $url_tag = "";
+ }
+
+ # Tag cloud
+ my $show_ctags = gitweb_check_feature('ctags');
+ if ($show_ctags) {
+ my $ctags = git_get_project_ctags($project);
+ if (%$ctags) {
+ # without ability to add tags, don't show if there are none
+ my $cloud = git_populate_project_tagcloud($ctags);
+ print "
" .
+ "
content tags
" .
+ "
".git_show_project_tagcloud($cloud, 48)."
" .
+ "
\n";
+ }
+ }
+
+ print "
\n";
+
+ # If XSS prevention is on, we don't include README.html.
+ # TODO: Allow a readme in some safe format.
+ if (!$prevent_xss && -s "$projectroot/$project/README.html") {
+ print "
";
+ git_footer_html();
+}
+
+sub git_tree {
+ if (!defined $hash_base) {
+ $hash_base = "HEAD";
+ }
+ if (!defined $hash) {
+ if (defined $file_name) {
+ $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
+ } else {
+ $hash = $hash_base;
+ }
+ }
+ die_error(404, "No such tree") unless defined($hash);
+
+ my $show_sizes = gitweb_check_feature('show-sizes');
+ my $have_blame = gitweb_check_feature('blame');
+
+ my @entries = ();
+ {
+ local $/ = "\0";
+ open my $fd, "-|", git_cmd(), "ls-tree", '-z',
+ ($show_sizes ? '-l' : ()), @extra_options, $hash
+ or die_error(500, "Open git-ls-tree failed");
+ @entries = map { chomp; $_ } <$fd>;
+ close $fd
+ or die_error(404, "Reading tree failed");
+ }
+
+ # Sort entries: directories first, then files, alphabetically within each group
+ @entries = sort {
+ my %a_parsed = parse_ls_tree_line($a, -z => 1, -l => $show_sizes);
+ my %b_parsed = parse_ls_tree_line($b, -z => 1, -l => $show_sizes);
+
+ # Compare types: tree (directory) comes before blob (file)
+ my $type_cmp = ($b_parsed{'type'} eq 'tree') <=> ($a_parsed{'type'} eq 'tree');
+ return $type_cmp if $type_cmp != 0;
+
+ # If same type, sort alphabetically by name
+ return lc($a_parsed{'name'}) cmp lc($b_parsed{'name'});
+ } @entries;
+
+ my $refs = git_get_references();
+ my $ref = format_ref_marker($refs, $hash_base);
+ git_header_html();
+ my $basedir = '';
+ if (defined $hash_base && (my %co = parse_commit($hash_base))) {
+ my @views_nav = ();
+ if (defined $file_name) {
+ push @views_nav,
+ $cgi->a({-href => href(action=>"history", -replay=>1)},
+ "history"),
+ $cgi->a({-href => href(action=>"tree",
+ hash_base=>"HEAD", file_name=>$file_name)},
+ "HEAD"),
+ }
+ my $snapshot_links = format_snapshot_links($hash);
+ if (defined $snapshot_links) {
+ # FIXME: Should be available when we have no hash base as well.
+ push @views_nav, $snapshot_links;
+ }
+ git_print_page_nav('tree','', $hash_base, undef, undef,
+ join(' | ', @views_nav));
+ git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
+ } else {
+ undef $hash_base;
+ print "
\n";
+ print "
\n";
+ print "
".esc_html($hash)."
\n";
+ }
+ if (defined $file_name) {
+ $basedir = $file_name;
+ if ($basedir ne '' && substr($basedir, -1) ne '/') {
+ $basedir .= '/';
+ }
+ git_print_page_path($file_name, 'tree', $hash_base);
+ }
+ print "
\n";
+ print "
\n";
+ my $alternate = 1;
+ # '..' (top directory) link if possible
+ if (defined $hash_base &&
+ defined $file_name && $file_name =~ m![^/]+$!) {
+ if ($alternate) {
+ print "
\n";
+ } else {
+ print "
\n";
+ }
+ $alternate ^= 1;
+
+ my $up = $file_name;
+ $up =~ s!/?[^/]+$!!;
+ undef $up unless $up;
+ # based on git_print_tree_entry
+ print '
\n"; # class="page_body"
+ git_footer_html();
+
+ } elsif ($format eq 'plain') {
+ local $/ = undef;
+ print <$fd>;
+ close $fd
+ or print "Reading git-diff-tree failed\n";
+ } elsif ($format eq 'patch') {
+ local $/ = undef;
+ print <$fd>;
+ close $fd
+ or print "Reading git-format-patch failed\n";
+ }
+}
+
+sub git_commitdiff_plain {
+ git_commitdiff(-format => 'plain');
+}
+
+# format-patch-style patches
+sub git_patch {
+ git_commitdiff(-format => 'patch', -single => 1);
+}
+
+sub git_patches {
+ git_commitdiff(-format => 'patch');
+}
+
+sub git_history {
+ git_log_generic('history', \&git_history_body,
+ $hash_base, $hash_parent_base,
+ $file_name, $hash);
+}
+
+sub git_search {
+ $searchtype ||= 'commit';
+
+ # check if appropriate features are enabled
+ gitweb_check_feature('search')
+ or die_error(403, "Search is disabled");
+ if ($searchtype eq 'pickaxe') {
+ # pickaxe may take all resources of your box and run for several minutes
+ # with every query - so decide by yourself how public you make this feature
+ gitweb_check_feature('pickaxe')
+ or die_error(403, "Pickaxe search is disabled");
+ }
+ if ($searchtype eq 'grep') {
+ # grep search might be potentially CPU-intensive, too
+ gitweb_check_feature('grep')
+ or die_error(403, "Grep search is disabled");
+ }
+
+ if (!defined $searchtext) {
+ die_error(400, "Text field is empty");
+ }
+ if (!defined $hash) {
+ $hash = git_get_head_hash($project);
+ }
+ my %co = parse_commit($hash);
+ if (!%co) {
+ die_error(404, "Unknown commit object");
+ }
+ if (!defined $page) {
+ $page = 0;
+ }
+
+ if ($searchtype eq 'commit' ||
+ $searchtype eq 'author' ||
+ $searchtype eq 'committer') {
+ git_search_message(%co);
+ } elsif ($searchtype eq 'pickaxe') {
+ git_search_changes(%co);
+ } elsif ($searchtype eq 'grep') {
+ git_search_files(%co);
+ } else {
+ die_error(400, "Unknown search type");
+ }
+}
+
+sub git_search_help {
+ git_header_html();
+ git_print_page_nav('','', $hash,$hash,$hash);
+ print <Pattern is by default a normal string that is matched precisely (but without
+regard to case, except in the case of pickaxe). However, when you check the re checkbox,
+the pattern entered is recognized as the POSIX extended
+regular expression (also case
+insensitive).
+
+
commit
+
The commit messages and authorship information will be scanned for the given pattern.
+EOT
+ my $have_grep = gitweb_check_feature('grep');
+ if ($have_grep) {
+ print <grep
+
All files in the currently selected tree (HEAD unless you are explicitly browsing
+ a different one) are searched for the given pattern. On large trees, this search can take
+a while and put some strain on the server, so please use it with some consideration. Note that
+due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
+case-sensitive.
+EOT
+ }
+ print <author
+
Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.
+
committer
+
Name and e-mail of the committer and date of commit will be scanned for the given pattern.
+EOT
+ my $have_pickaxe = gitweb_check_feature('pickaxe');
+ if ($have_pickaxe) {
+ print <pickaxe
+
All commits that caused the string to appear or disappear from any file (changes that
+added, removed or "modified" the string) will be listed. This search can take a while and
+takes a lot of strain on the server, so please use it wisely. Note that since you may be
+interested even in changes just changing the case as well, this search is case sensitive.
+EOT
+ }
+ print "
\n";
+ git_footer_html();
+}
+
+sub git_shortlog {
+ git_log_generic('shortlog', \&git_shortlog_body,
+ $hash, $hash_parent);
+}
+
+## ......................................................................
+## feeds (RSS, Atom; OPML)
+
+sub git_feed {
+ my $format = shift || 'atom';
+ my $have_blame = gitweb_check_feature('blame');
+
+ # Atom: http://www.atomenabled.org/developers/syndication/
+ # RSS: https://web.archive.org/web/20030729001534/http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+ if ($format ne 'rss' && $format ne 'atom') {
+ die_error(400, "Unknown web feed format");
+ }
+
+ # log/feed of current (HEAD) branch, log of given branch, history of file/directory
+ my $head = $hash || 'HEAD';
+ my @commitlist = parse_commits($head, 150, 0, $file_name);
+
+ my %latest_commit;
+ my %latest_date;
+ my $content_type = "application/$format+xml";
+ if (defined $cgi->http('HTTP_ACCEPT') &&
+ $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
+ # browser (feed reader) prefers text/xml
+ $content_type = 'text/xml';
+ }
+ if (defined($commitlist[0])) {
+ %latest_commit = %{$commitlist[0]};
+ my $latest_epoch = $latest_commit{'committer_epoch'};
+ exit_if_unmodified_since($latest_epoch);
+ %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
+ }
+ print $cgi->header(
+ -type => $content_type,
+ -charset => 'utf-8',
+ %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
+ -status => '200 OK');
+
+ # Optimization: skip generating the body if client asks only
+ # for Last-Modified date.
+ return if ($cgi->request_method() eq 'HEAD');
+
+ # header variables
+ my $title = "$site_name - $project/$action";
+ my $feed_type = 'log';
+ if (defined $hash) {
+ $title .= " - '$hash'";
+ $feed_type = 'branch log';
+ if (defined $file_name) {
+ $title .= " :: $file_name";
+ $feed_type = 'history';
+ }
+ } elsif (defined $file_name) {
+ $title .= " - $file_name";
+ $feed_type = 'history';
+ }
+ $title .= " $feed_type";
+ $title = esc_html($title);
+ my $descr = git_get_project_description($project);
+ if (defined $descr) {
+ $descr = esc_html($descr);
+ } else {
+ $descr = "$project " .
+ ($format eq 'rss' ? 'RSS' : 'Atom') .
+ " feed";
+ }
+ my $owner = git_get_project_owner($project);
+ $owner = esc_html($owner);
+
+ #header
+ my $alt_url;
+ if (defined $file_name) {
+ $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
+ } elsif (defined $hash) {
+ $alt_url = href(-full=>1, action=>"log", hash=>$hash);
+ } else {
+ $alt_url = href(-full=>1, action=>"summary");
+ }
+ $alt_url = esc_attr($alt_url);
+ print qq!\n!;
+ if ($format eq 'rss') {
+ print <
+
+XML
+ print "$title\n" .
+ "$alt_url\n" .
+ "$descr\n" .
+ "en\n" .
+ # project owner is responsible for 'editorial' content
+ "$owner\n";
+ if (defined $logo || defined $favicon) {
+ # prefer the logo to the favicon, since RSS
+ # doesn't allow both
+ my $img = esc_url($logo || $favicon);
+ print "\n" .
+ "$img\n" .
+ "$title\n" .
+ "$alt_url\n" .
+ "\n";
+ }
+ if (%latest_date) {
+ print "$latest_date{'rfc2822'}\n";
+ print "$latest_date{'rfc2822'}\n";
+ }
+ print "gitweb v.$version/$git_version\n";
+ } elsif ($format eq 'atom') {
+ print <
+XML
+ print "$title\n" .
+ "$descr\n" .
+ '' . "\n" .
+ '' . "\n" .
+ "" . esc_url(href(-full=>1)) . "\n" .
+ # use project owner for feed author
+ "$owner\n";
+ if (defined $favicon) {
+ print "" . esc_url($favicon) . "\n";
+ }
+ if (defined $logo) {
+ # not twice as wide as tall: 72 x 27 pixels
+ print "" . esc_url($logo) . "\n";
+ }
+ if (! %latest_date) {
+ # dummy date to keep the feed valid until commits trickle in:
+ print "1970-01-01T00:00:00Z\n";
+ } else {
+ print "$latest_date{'iso-8601'}\n";
+ }
+ print "gitweb\n";
+ }
+
+ # contents
+ for (my $i = 0; $i <= $#commitlist; $i++) {
+ my %co = %{$commitlist[$i]};
+ my $commit = $co{'id'};
+ # we read 150, we always show 30 and the ones more recent than 48 hours
+ if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
+ last;
+ }
+ my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
+
+ # get list of changed files
+ open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+ $co{'parent'} || "--root",
+ $co{'id'}, "--", (defined $file_name ? $file_name : ())
+ or next;
+ my @difftree = map { chomp; $_ } <$fd>;
+ close $fd
+ or next;
+
+ # print element (entry, item)
+ my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
+ if ($format eq 'rss') {
+ print "\n" .
+ "" . esc_html($co{'title'}) . "\n" .
+ "" . esc_html($co{'author'}) . "\n" .
+ "$cd{'rfc2822'}\n" .
+ "$co_url\n" .
+ "" . esc_html($co_url) . "\n" .
+ "" . esc_html($co{'title'}) . "\n" .
+ "" .
+ "\n" .
+ "" . esc_html($co{'title'}) . "\n" .
+ "$cd{'iso-8601'}\n" .
+ "\n" .
+ " " . esc_html($co{'author_name'}) . "\n";
+ if ($co{'author_email'}) {
+ print " " . esc_html($co{'author_email'}) . "\n";
+ }
+ print "\n" .
+ # use committer for contributor
+ "\n" .
+ " " . esc_html($co{'committer_name'}) . "\n";
+ if ($co{'committer_email'}) {
+ print " " . esc_html($co{'committer_email'}) . "\n";
+ }
+ print "\n" .
+ "$cd{'iso-8601'}\n" .
+ "\n" .
+ "" . esc_html($co_url) . "\n" .
+ "\n" .
+ "
+ Here IÂ publish stuff I make. If you came here for my blogs or other personal stuff,
+ you want to visit the
+ root domain.
+ If you would like to clone a project to your local machine, you can use command:
+
+ However, you wonât be able to push any changes back to server.
+ If you are interested in contributing to a project, check out the
+ server rules and
+ if they seem reasonable to you, follow this
+ contributorâs guide.
+
+ Feel free to use my
+ GPG key
+ for encryption of your messages. If you would like to encrypt your message, but
+ don't know how, follow this
+ tutorial.
+ I respond to emails using
+ inline replies.
+
+
+
+
diff --git a/Git-server/Gitweb/static/gitweb.css b/Git-server/Gitweb/static/gitweb.css
new file mode 100644
index 0000000..7079fbd
--- /dev/null
+++ b/Git-server/Gitweb/static/gitweb.css
@@ -0,0 +1,707 @@
+html {
+ background-image: url("https://dpolakovic.space/Pictures/sky.jpg")
+}
+
+
+body {
+ font-family: sans-serif;
+ font-size: 14px;
+ border: solid #d9d8d1;
+ border-width: 1px;
+ /* margin: 10px; */
+ width:1000px;
+ margin: 0 auto;
+ margin-bottom: 20px;
+ background-color: #ffffff;
+ color: #000000;
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+a {
+ color: #0000cc;
+}
+
+a:hover, a:visited, a:active {
+ color: #551a8b;
+}
+
+a:hover {
+ color: #4682b4;
+}
+
+span.cntrl {
+ border: dashed #aaaaaa;
+ border-width: 1px;
+ padding: 0px 2px 0px 2px;
+ margin: 0px 2px 0px 2px;
+}
+
+img.logo {
+ float: right;
+ border-width: 0px;
+}
+
+img.avatar {
+ vertical-align: middle;
+}
+
+img.blob {
+ max-height: 100%;
+ max-width: 100%;
+}
+
+a.list img.avatar {
+ border-style: none;
+}
+
+div.page_header {
+ height: 25px;
+ padding: 8px;
+ font-size: 150%;
+ font-weight: bold;
+ background-color: #d9d8d1;
+}
+
+div.page_header a:visited, a.header {
+ color: #0000cc;
+}
+
+div.page_header a:hover {
+ color: #4682b4;
+}
+
+div.page_nav {
+ padding: 8px;
+}
+
+div.page_nav a:visited {
+ color: #551a8b;
+}
+
+div.page_path {
+ padding: 8px;
+ font-weight: bold;
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px;
+}
+
+div.page_footer {
+ height: 22px;
+ padding: 4px 8px;
+ background-color: #d9d8d1;
+}
+
+div.page_footer_text {
+ line-height: 22px;
+ float: left;
+ color: #555555;
+ font-style: italic;
+}
+
+div#generating_info {
+ margin: 4px;
+ font-size: smaller;
+ text-align: center;
+ color: #505050;
+}
+
+div.page_body {
+ padding: 8px;
+ font-family: monospace;
+}
+
+div.title, a.title {
+ display: block;
+ padding: 6px 8px;
+ font-weight: bold;
+ background-color: #edece6;
+ text-decoration: none;
+ color: #000000;
+}
+
+div.readme {
+ padding: 8px;
+}
+
+a.title:hover {
+ background-color: #d9d8d1;
+}
+
+div.title_text {
+ padding: 6px 0px;
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px;
+ font-family: monospace;
+}
+
+div.log_body {
+ padding: 8px 8px 8px 150px;
+}
+
+span.age {
+ position: relative;
+ float: left;
+ width: 142px;
+ font-style: italic;
+}
+
+span.signoff {
+ color: #888888;
+}
+
+div.log_link {
+ padding: 0px 8px;
+ font-size: 70%;
+ font-family: sans-serif;
+ font-style: normal;
+ position: relative;
+ float: left;
+ width: 136px;
+}
+
+div.list_head {
+ padding: 6px 8px 4px;
+ border: solid #d9d8d1;
+ border-width: 1px 0px 0px;
+ font-style: italic;
+}
+
+.author_date, .author {
+ font-style: italic;
+}
+
+div.author_date {
+ padding: 8px;
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px 0px;
+}
+
+a.list {
+ text-decoration: none;
+ color: #000000;
+}
+
+a.subject, a.name {
+ font-weight: bold;
+}
+
+table.tags a.subject {
+ font-weight: normal;
+}
+
+a.list:hover {
+ text-decoration: underline;
+ color: #4682b4;
+}
+
+a.text {
+ text-decoration: none;
+ color: #0000cc;
+}
+
+a.text:visited {
+ text-decoration: none;
+ color: #551a8b;
+}
+
+a.text:hover {
+ text-decoration: underline;
+ color: #4682b4;
+}
+
+table {
+ padding: 8px 4px;
+ border-spacing: 0;
+}
+
+table.diff_tree {
+ font-family: monospace;
+}
+
+table.combined.diff_tree th {
+ text-align: center;
+}
+
+table.combined.diff_tree td {
+ padding-right: 24px;
+}
+
+table.combined.diff_tree th.link,
+table.combined.diff_tree td.link {
+ padding: 0px 2px;
+}
+
+table.combined.diff_tree td.nochange a {
+ color: #6666ff;
+}
+
+table.combined.diff_tree td.nochange a:hover,
+table.combined.diff_tree td.nochange a:visited {
+ color: #d06666;
+}
+
+table.blame {
+ border-collapse: collapse;
+}
+
+table.blame td {
+ padding: 0px 5px;
+ font-size: 100%;
+ vertical-align: top;
+}
+
+th {
+ padding: 2px 5px;
+ font-size: 100%;
+ text-align: left;
+}
+
+/* do not change row style on hover for 'blame' view */
+tr.light,
+table.blame .light:hover {
+ background-color: #ffffff;
+}
+
+tr.dark,
+table.blame .dark:hover {
+ background-color: #f6f6f0;
+}
+
+/* currently both use the same, but it can change */
+tr.light:hover,
+tr.dark:hover {
+ background-color: #edece6;
+}
+
+/* boundary commits in 'blame' view */
+/* and commits without "previous" */
+tr.boundary td.sha1,
+tr.no-previous td.linenr {
+ font-weight: bold;
+}
+
+/* for 'blame_incremental', during processing */
+tr.color1 { background-color: #f6fff6; }
+tr.color2 { background-color: #f6f6ff; }
+tr.color3 { background-color: #fff6f6; }
+
+td {
+ padding: 2px 5px;
+ font-size: 100%;
+ vertical-align: top;
+}
+
+td.link, td.selflink {
+ padding: 2px 5px;
+ font-family: sans-serif;
+ font-size: 70%;
+}
+
+td.selflink {
+ padding-right: 0px;
+}
+
+td.sha1 {
+ font-family: monospace;
+}
+
+.error {
+ color: red;
+ background-color: yellow;
+}
+
+td.current_head {
+ text-decoration: underline;
+}
+
+td.category {
+ background-color: #d9d8d1;
+ border-top: 1px solid #000000;
+ border-left: 1px solid #000000;
+ font-weight: bold;
+}
+
+table.diff_tree span.file_status.new {
+ color: #008000;
+}
+
+table.diff_tree span.file_status.deleted {
+ color: #c00000;
+}
+
+table.diff_tree span.file_status.moved,
+table.diff_tree span.file_status.mode_chnge {
+ color: #777777;
+}
+
+table.diff_tree span.file_status.copied {
+ color: #70a070;
+}
+
+/* noage: "No commits" */
+table.project_list td.noage {
+ color: #808080;
+ font-style: italic;
+}
+
+/* age2: 60*60*24*2 <= age */
+table.project_list td.age2, table.blame td.age2 {
+ font-style: italic;
+}
+
+/* age1: 60*60*2 <= age < 60*60*24*2 */
+table.project_list td.age1 {
+ color: #009900;
+ font-style: italic;
+}
+
+table.blame td.age1 {
+ color: #009900;
+ background: transparent;
+}
+
+/* age0: age < 60*60*2 */
+table.project_list td.age0 {
+ color: #009900;
+ font-style: italic;
+ font-weight: bold;
+}
+
+table.blame td.age0 {
+ color: #009900;
+ background: transparent;
+ font-weight: bold;
+}
+
+td.pre, div.pre, div.diff {
+ font-family: monospace;
+ font-size: 12px;
+ white-space: pre;
+}
+
+td.mode {
+ font-family: monospace;
+}
+
+/* progress of blame_interactive */
+div#progress_bar {
+ height: 2px;
+ margin-bottom: -2px;
+ background-color: #d8d9d0;
+}
+div#progress_info {
+ float: right;
+ text-align: right;
+}
+
+/* format of (optional) objects size in 'tree' view */
+td.size {
+ font-family: monospace;
+ text-align: right;
+}
+
+/* styling of diffs (patchsets): commitdiff and blobdiff views */
+div.diff.header,
+div.diff.extended_header {
+ white-space: normal;
+}
+
+div.diff.header {
+ font-weight: bold;
+
+ background-color: #edece6;
+
+ margin-top: 4px;
+ padding: 4px 0px 2px 0px;
+ border: solid #d9d8d1;
+ border-width: 1px 0px 1px 0px;
+}
+
+div.diff.header a.path {
+ text-decoration: underline;
+}
+
+div.diff.extended_header,
+div.diff.extended_header a.path,
+div.diff.extended_header a.hash {
+ color: #777777;
+}
+
+div.diff.extended_header .info {
+ color: #b0b0b0;
+}
+
+div.diff.extended_header {
+ background-color: #f6f5ee;
+ padding: 2px 0px 2px 0px;
+}
+
+div.diff a.list,
+div.diff a.path,
+div.diff a.hash {
+ text-decoration: none;
+}
+
+div.diff a.list:hover,
+div.diff a.path:hover,
+div.diff a.hash:hover {
+ text-decoration: underline;
+}
+
+div.diff.to_file a.path,
+div.diff.to_file {
+ color: #007000;
+}
+
+div.diff.add {
+ color: #008800;
+}
+
+div.diff.add span.marked {
+ background-color: #aaffaa;
+}
+
+div.diff.from_file a.path,
+div.diff.from_file {
+ color: #aa0000;
+}
+
+div.diff.rem {
+ color: #cc0000;
+}
+
+div.diff.rem span.marked {
+ background-color: #ffaaaa;
+}
+
+div.diff.chunk_header a,
+div.diff.chunk_header {
+ color: #990099;
+}
+
+div.diff.chunk_header {
+ border: dotted #ffe0ff;
+ border-width: 1px 0px 0px 0px;
+ margin-top: 2px;
+}
+
+div.diff.chunk_header span.chunk_info {
+ background-color: #ffeeff;
+}
+
+div.diff.chunk_header span.section {
+ color: #aa22aa;
+}
+
+div.diff.incomplete {
+ color: #cccccc;
+}
+
+div.diff.nodifferences {
+ font-weight: bold;
+ color: #600000;
+}
+
+/* side-by-side diff */
+div.chunk_block {
+ overflow: hidden;
+}
+
+div.chunk_block div.old {
+ float: left;
+ width: 50%;
+ overflow: hidden;
+}
+
+div.chunk_block div.new {
+ margin-left: 50%;
+ width: 50%;
+}
+
+div.chunk_block.rem div.old div.diff.rem {
+ background-color: #fff5f5;
+}
+div.chunk_block.add div.new div.diff.add {
+ background-color: #f8fff8;
+}
+div.chunk_block.chg div div.diff {
+ background-color: #fffff0;
+}
+div.chunk_block.ctx div div.diff.ctx {
+ color: #404040;
+}
+
+
+div.index_include {
+ border: solid #d9d8d1;
+ border-width: 0px 0px 1px;
+ padding: 12px 8px;
+}
+
+div.search {
+ font-size: 100%;
+ font-weight: normal;
+ margin: 4px 8px;
+ float: right;
+ top: 56px;
+ right: 12px
+}
+
+/*
+div.projsearch {
+ text-align: center;
+ margin: 20px 0px;
+}
+*/
+
+div.projsearch {
+ display: none;
+}
+
+div.projsearch form {
+ margin-bottom: 2px;
+}
+
+td.linenr {
+ text-align: right;
+}
+
+a.linenr {
+ color: #999999;
+ text-decoration: none
+}
+
+a.rss_logo {
+ float: right;
+ padding: 3px 5px;
+ line-height: 10px;
+ border: 1px solid;
+ border-color: #fcc7a5 #7d3302 #3e1a01 #ff954e;
+ color: #ffffff;
+ background-color: #ff6600;
+ font-weight: bold;
+ font-family: sans-serif;
+ font-size: 70%;
+ text-align: center;
+ text-decoration: none;
+}
+
+a.rss_logo:hover {
+ background-color: #ee5500;
+}
+
+a.rss_logo.generic {
+ background-color: #ff8800;
+}
+
+a.rss_logo.generic:hover {
+ background-color: #ee7700;
+}
+
+span.refs span {
+ padding: 0px 4px;
+ font-size: 70%;
+ font-weight: normal;
+ border: 1px solid;
+ background-color: #ffaaff;
+ border-color: #ffccff #ff00ee #ff00ee #ffccff;
+}
+
+span.refs span a {
+ text-decoration: none;
+ color: inherit;
+}
+
+span.refs span a:hover {
+ text-decoration: underline;
+}
+
+span.refs span.indirect {
+ font-style: italic;
+}
+
+span.refs span.ref {
+ background-color: #aaaaff;
+ border-color: #ccccff #0033cc #0033cc #ccccff;
+}
+
+span.refs span.tag {
+ background-color: #ffffaa;
+ border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
+}
+
+span.refs span.head {
+ background-color: #aaffaa;
+ border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
+}
+
+span.atnight {
+ color: #cc0000;
+}
+
+span.match {
+ color: #e00000;
+}
+
+div.binary {
+ font-style: italic;
+}
+
+div.remote {
+ margin: .5em;
+ border: 1px solid #d9d8d1;
+ display: inline-block;
+}
+
+/* JavaScript-based timezone manipulation */
+
+.popup { /* timezone selection UI */
+ position: absolute;
+ /* "top: 0; right: 0;" would be better, if not for bugs in browsers */
+ top: 0; left: 0;
+ border: 1px solid;
+ padding: 2px;
+ background-color: #f0f0f0;
+ font-style: normal;
+ color: #000000;
+ cursor: auto;
+}
+
+.close-button { /* close timezone selection UI without selecting */
+ /* float doesn't work within absolutely positioned container,
+ * if width of container is not set explicitly */
+ /* float: right; */
+ position: absolute;
+ top: 0px; right: 0px;
+ border: 1px solid green;
+ margin: 1px 1px 1px 1px;
+ padding-bottom: 2px;
+ width: 12px;
+ height: 10px;
+ font-size: 9px;
+ font-weight: bold;
+ text-align: center;
+ background-color: #fff0f0;
+ cursor: pointer;
+}
+
+
+/* Style definition generated by highlight 2.4.5, http://andre-simon.de/doku/highlight/en/highlight.php */
+
+/* Highlighting theme definition: */
+
+.num { color:#2928ff; }
+.esc { color:#ff00ff; }
+.str { color:#ff0000; }
+.dstr { color:#818100; }
+.slc { color:#838183; font-style:italic; }
+.com { color:#838183; font-style:italic; }
+.dir { color:#008200; }
+.sym { color:#000000; }
+.line { color:#555555; }
+.kwa { color:#000000; font-weight:bold; }
+.kwb { color:#830000; }
+.kwc { color:#000000; font-weight:bold; }
+.kwd { color:#010181; }
+
diff --git a/Git-server/Gitweb/static/gitweb.js b/Git-server/Gitweb/static/gitweb.js
new file mode 100644
index 0000000..e676aae
--- /dev/null
+++ b/Git-server/Gitweb/static/gitweb.js
@@ -0,0 +1,1579 @@
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview Generic JavaScript code (helper functions)
+ * @license GPLv2 or later
+ */
+
+
+/* ============================================================ */
+/* ............................................................ */
+/* Padding */
+
+/**
+ * pad INPUT on the left with STR that is assumed to have visible
+ * width of single character (for example nonbreakable spaces),
+ * to WIDTH characters
+ *
+ * example: padLeftStr(12, 3, '\u00A0') == '\u00A012'
+ * ('\u00A0' is nonbreakable space)
+ *
+ * @param {Number|String} input: number to pad
+ * @param {Number} width: visible width of output
+ * @param {String} str: string to prefix to string, defaults to '\u00A0'
+ * @returns {String} INPUT prefixed with STR x (WIDTH - INPUT.length)
+ */
+function padLeftStr(input, width, str) {
+ var prefix = '';
+ if (typeof str === 'undefined') {
+ ch = '\u00A0'; // using ' ' doesn't work in all browsers
+ }
+
+ width -= input.toString().length;
+ while (width > 0) {
+ prefix += str;
+ width--;
+ }
+ return prefix + input;
+}
+
+/**
+ * Pad INPUT on the left to WIDTH, using given padding character CH,
+ * for example padLeft('a', 3, '_') is '__a'
+ * padLeft(4, 2) is '04' (same as padLeft(4, 2, '0'))
+ *
+ * @param {String} input: input value converted to string.
+ * @param {Number} width: desired length of output.
+ * @param {String} ch: single character to prefix to string, defaults to '0'.
+ *
+ * @returns {String} Modified string, at least SIZE length.
+ */
+function padLeft(input, width, ch) {
+ var s = input + "";
+ if (typeof ch === 'undefined') {
+ ch = '0';
+ }
+
+ while (s.length < width) {
+ s = ch + s;
+ }
+ return s;
+}
+
+
+/* ............................................................ */
+/* Handling browser incompatibilities */
+
+/**
+ * Create XMLHttpRequest object in cross-browser way
+ * @returns XMLHttpRequest object, or null
+ */
+function createRequestObject() {
+ try {
+ return new XMLHttpRequest();
+ } catch (e) {}
+ try {
+ return window.createRequest();
+ } catch (e) {}
+ try {
+ return new ActiveXObject("Msxml2.XMLHTTP");
+ } catch (e) {}
+ try {
+ return new ActiveXObject("Microsoft.XMLHTTP");
+ } catch (e) {}
+
+ return null;
+}
+
+
+/**
+ * Insert rule giving specified STYLE to given SELECTOR at the end of
+ * first CSS stylesheet.
+ *
+ * @param {String} selector: CSS selector, e.g. '.class'
+ * @param {String} style: rule contents, e.g. 'background-color: red;'
+ */
+function addCssRule(selector, style) {
+ var stylesheet = document.styleSheets[0];
+
+ var theRules = [];
+ if (stylesheet.cssRules) { // W3C way
+ theRules = stylesheet.cssRules;
+ } else if (stylesheet.rules) { // IE way
+ theRules = stylesheet.rules;
+ }
+
+ if (stylesheet.insertRule) { // W3C way
+ stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length);
+ } else if (stylesheet.addRule) { // IE way
+ stylesheet.addRule(selector, style);
+ }
+}
+
+
+/* ............................................................ */
+/* Support for legacy browsers */
+
+/**
+ * Provides getElementsByClassName method, if there is no native
+ * implementation of this method.
+ *
+ * NOTE that there are limits and differences compared to native
+ * getElementsByClassName as defined by e.g.:
+ * https://developer.mozilla.org/en/DOM/document.getElementsByClassName
+ * https://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-getelementsbyclassname
+ * https://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-document-getelementsbyclassname
+ *
+ * Namely, this implementation supports only single class name as
+ * argument and not set of space-separated tokens representing classes,
+ * it returns Array of nodes rather than live NodeList, and has
+ * additional optional argument where you can limit search to given tags
+ * (via getElementsByTagName).
+ *
+ * Based on
+ * https://code.google.com/p/getelementsbyclassname/
+ * http://www.dustindiaz.com/getelementsbyclass/
+ * https://stackoverflow.com/questions/1818865/do-we-have-getelementsbyclassname-in-javascript
+ *
+ * See also https://johnresig.com/blog/getelementsbyclassname-speed-comparison/
+ *
+ * @param {String} class: name of _single_ class to find
+ * @param {String} [taghint] limit search to given tags
+ * @returns {Node[]} array of matching elements
+ */
+if (!('getElementsByClassName' in document)) {
+ document.getElementsByClassName = function (classname, taghint) {
+ taghint = taghint || "*";
+ var elements = (taghint === "*" && document.all) ?
+ document.all :
+ document.getElementsByTagName(taghint);
+ var pattern = new RegExp("(^|\\s)" + classname + "(\\s|$)");
+ var matches= [];
+ for (var i = 0, j = 0, n = elements.length; i < n; i++) {
+ var el= elements[i];
+ if (el.className && pattern.test(el.className)) {
+ // matches.push(el);
+ matches[j] = el;
+ j++;
+ }
+ }
+ return matches;
+ };
+} // end if
+
+
+/* ............................................................ */
+/* unquoting/unescaping filenames */
+
+/**#@+
+ * @constant
+ */
+var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g;
+var octEscRe = /^[0-7]{1,3}$/;
+var maybeQuotedRe = /^\"(.*)\"$/;
+/**#@-*/
+
+/**
+ * unquote maybe C-quoted filename (as used by git, i.e. it is
+ * in double quotes '"' if there is any escape character used)
+ * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a'
+ *
+ * @param {String} str: git-quoted string
+ * @returns {String} Unquoted and unescaped string
+ *
+ * @globals escCodeRe, octEscRe, maybeQuotedRe
+ */
+function unquote(str) {
+ function unq(seq) {
+ var es = {
+ // character escape codes, aka escape sequences (from C)
+ // replacements are to some extent JavaScript specific
+ t: "\t", // tab (HT, TAB)
+ n: "\n", // newline (NL)
+ r: "\r", // return (CR)
+ f: "\f", // form feed (FF)
+ b: "\b", // backspace (BS)
+ a: "\x07", // alarm (bell) (BEL)
+ e: "\x1B", // escape (ESC)
+ v: "\v" // vertical tab (VT)
+ };
+
+ if (seq.search(octEscRe) !== -1) {
+ // octal char sequence
+ return String.fromCharCode(parseInt(seq, 8));
+ } else if (seq in es) {
+ // C escape sequence, aka character escape code
+ return es[seq];
+ }
+ // quoted ordinary character
+ return seq;
+ }
+
+ var match = str.match(maybeQuotedRe);
+ if (match) {
+ str = match[1];
+ // perhaps str = eval('"'+str+'"'); would be enough?
+ str = str.replace(escCodeRe,
+ function (substr, p1, offset, s) { return unq(p1); });
+ }
+ return str;
+}
+
+/* end of common-lib.js */
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview Datetime manipulation: parsing and formatting
+ * @license GPLv2 or later
+ */
+
+
+/* ............................................................ */
+/* parsing and retrieving datetime related information */
+
+/**
+ * used to extract hours and minutes from timezone info, e.g '-0900'
+ * @constant
+ */
+var tzRe = /^([+\-])([0-9][0-9])([0-9][0-9])$/;
+
+/**
+ * convert numeric timezone +/-ZZZZ to offset from UTC in seconds
+ *
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @returns {Number} offset from UTC in seconds for timezone
+ *
+ * @globals tzRe
+ */
+function timezoneOffset(timezoneInfo) {
+ var match = tzRe.exec(timezoneInfo);
+ var tz_sign = (match[1] === '-' ? -1 : +1);
+ var tz_hour = parseInt(match[2],10);
+ var tz_min = parseInt(match[3],10);
+
+ return tz_sign*(((tz_hour*60) + tz_min)*60);
+}
+
+/**
+ * return local (browser) timezone as offset from UTC in seconds
+ *
+ * @returns {Number} offset from UTC in seconds for local timezone
+ */
+function localTimezoneOffset() {
+ // getTimezoneOffset returns the time-zone offset from UTC,
+ // in _minutes_, for the current locale
+ return ((new Date()).getTimezoneOffset() * -60);
+}
+
+/**
+ * return local (browser) timezone as numeric timezone '(+|-)HHMM'
+ *
+ * @returns {String} locat timezone as -/+ZZZZ
+ */
+function localTimezoneInfo() {
+ var tzOffsetMinutes = (new Date()).getTimezoneOffset() * -1;
+
+ return formatTimezoneInfo(0, tzOffsetMinutes);
+}
+
+
+/**
+ * Parse RFC-2822 date into a Unix timestamp (into epoch)
+ *
+ * @param {String} date: date in RFC-2822 format, e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
+ * @returns {Number} epoch i.e. seconds since '00:00:00 1970-01-01 UTC'
+ */
+function parseRFC2822Date(date) {
+ // Date.parse accepts the IETF standard (RFC 1123 Section 5.2.14 and elsewhere)
+ // date syntax, which is defined in RFC 2822 (obsoletes RFC 822)
+ // and returns number of _milli_seconds since January 1, 1970, 00:00:00 UTC
+ return Date.parse(date) / 1000;
+}
+
+
+/* ............................................................ */
+/* formatting date */
+
+/**
+ * format timezone offset as numerical timezone '(+|-)HHMM' or '(+|-)HH:MM'
+ *
+ * @param {Number} hours: offset in hours, e.g. 2 for '+0200'
+ * @param {Number} [minutes] offset in minutes, e.g. 30 for '-4030';
+ * it is split into hours if not 0 <= minutes < 60,
+ * for example 1200 would give '+0100';
+ * defaults to 0
+ * @param {String} [sep] separator between hours and minutes part,
+ * default is '', might be ':' for W3CDTF (rfc-3339)
+ * @returns {String} timezone in '(+|-)HHMM' or '(+|-)HH:MM' format
+ */
+function formatTimezoneInfo(hours, minutes, sep) {
+ minutes = minutes || 0; // to be able to use formatTimezoneInfo(hh)
+ sep = sep || ''; // default format is +/-ZZZZ
+
+ if (minutes < 0 || minutes > 59) {
+ hours = minutes > 0 ? Math.floor(minutes / 60) : Math.ceil(minutes / 60);
+ minutes = Math.abs(minutes - 60*hours); // sign of minutes is sign of hours
+ // NOTE: this works correctly because there is no UTC-00:30 timezone
+ }
+
+ var tzSign = hours >= 0 ? '+' : '-';
+ if (hours < 0) {
+ hours = -hours; // sign is stored in tzSign
+ }
+
+ return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0');
+}
+
+/**
+ * translate 'utc' and 'local' to numerical timezone
+ * @param {String} timezoneInfo: might be 'utc' or 'local' (browser)
+ */
+function normalizeTimezoneInfo(timezoneInfo) {
+ switch (timezoneInfo) {
+ case 'utc':
+ return '+0000';
+ case 'local': // 'local' is browser timezone
+ return localTimezoneInfo();
+ }
+ return timezoneInfo;
+}
+
+
+/**
+ * return date in local time formatted in iso-8601 like format
+ * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200'
+ *
+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @returns {String} date in local time in iso-8601 like format
+ */
+function formatDateISOLocal(epoch, timezoneInfo) {
+ // date corrected by timezone
+ var localDate = new Date(1000 * (epoch +
+ timezoneOffset(timezoneInfo)));
+ var localDateStr = // e.g. '2005-08-07'
+ localDate.getUTCFullYear() + '-' +
+ padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' +
+ padLeft(localDate.getUTCDate(), 2, '0');
+ var localTimeStr = // e.g. '21:49:46'
+ padLeft(localDate.getUTCHours(), 2, '0') + ':' +
+ padLeft(localDate.getUTCMinutes(), 2, '0') + ':' +
+ padLeft(localDate.getUTCSeconds(), 2, '0');
+
+ return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;
+}
+
+/**
+ * return date in local time formatted in rfc-2822 format
+ * e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
+ *
+ * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC'
+ * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM'
+ * @param {Boolean} [padDay] e.g. 'Sun, 07 Aug' if true, 'Sun, 7 Aug' otherwise
+ * @returns {String} date in local time in rfc-2822 format
+ */
+function formatDateRFC2882(epoch, timezoneInfo, padDay) {
+ // A short textual representation of a month, three letters
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+ // A textual representation of a day, three letters
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ // date corrected by timezone
+ var localDate = new Date(1000 * (epoch +
+ timezoneOffset(timezoneInfo)));
+ var localDateStr = // e.g. 'Sun, 7 Aug 2005' or 'Sun, 07 Aug 2005'
+ days[localDate.getUTCDay()] + ', ' +
+ (padDay ? padLeft(localDate.getUTCDate(),2,'0') : localDate.getUTCDate()) + ' ' +
+ months[localDate.getUTCMonth()] + ' ' +
+ localDate.getUTCFullYear();
+ var localTimeStr = // e.g. '21:49:46'
+ padLeft(localDate.getUTCHours(), 2, '0') + ':' +
+ padLeft(localDate.getUTCMinutes(), 2, '0') + ':' +
+ padLeft(localDate.getUTCSeconds(), 2, '0');
+
+ return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo;
+}
+
+/* end of datetime.js */
+/**
+ * @fileOverview Accessing cookies from JavaScript
+ * @license GPLv2 or later
+ */
+
+/*
+ * Based on subsection "Cookies in JavaScript" of "Professional
+ * JavaScript for Web Developers" by Nicholas C. Zakas and cookie
+ * plugin from jQuery (dual licensed under the MIT and GPL licenses)
+ */
+
+
+/**
+ * Create a cookie with the given name and value,
+ * and other optional parameters.
+ *
+ * @example
+ * setCookie('foo', 'bar'); // will be deleted when browser exits
+ * setCookie('foo', 'bar', { expires: new Date(Date.parse('Jan 1, 2012')) });
+ * setCookie('foo', 'bar', { expires: 7 }); // 7 days = 1 week
+ * setCookie('foo', 'bar', { expires: 14, path: '/' });
+ *
+ * @param {String} sName: Unique name of a cookie (letters, numbers, underscores).
+ * @param {String} sValue: The string value stored in a cookie.
+ * @param {Object} [options] An object literal containing key/value pairs
+ * to provide optional cookie attributes.
+ * @param {String|Number|Date} [options.expires] Either literal string to be used as cookie expires,
+ * or an integer specifying the expiration date from now on in days,
+ * or a Date object to be used as cookie expiration date.
+ * If a negative value is specified or a date in the past),
+ * the cookie will be deleted.
+ * If set to null or omitted, the cookie will be a session cookie
+ * and will not be retained when the browser exits.
+ * @param {String} [options.path] Restrict access of a cookie to particular directory
+ * (default: path of page that created the cookie).
+ * @param {String} [options.domain] Override what web sites are allowed to access cookie
+ * (default: domain of page that created the cookie).
+ * @param {Boolean} [options.secure] If true, the secure attribute of the cookie will be set
+ * and the cookie would be accessible only from secure sites
+ * (cookie transmission will require secure protocol like HTTPS).
+ */
+function setCookie(sName, sValue, options) {
+ options = options || {};
+ if (sValue === null) {
+ sValue = '';
+ option.expires = 'delete';
+ }
+
+ var sCookie = sName + '=' + encodeURIComponent(sValue);
+
+ if (options.expires) {
+ var oExpires = options.expires, sDate;
+ if (oExpires === 'delete') {
+ sDate = 'Thu, 01 Jan 1970 00:00:00 GMT';
+ } else if (typeof oExpires === 'string') {
+ sDate = oExpires;
+ } else {
+ var oDate;
+ if (typeof oExpires === 'number') {
+ oDate = new Date();
+ oDate.setTime(oDate.getTime() + (oExpires * 24 * 60 * 60 * 1000)); // days to ms
+ } else {
+ oDate = oExpires;
+ }
+ sDate = oDate.toGMTString();
+ }
+ sCookie += '; expires=' + sDate;
+ }
+
+ if (options.path) {
+ sCookie += '; path=' + (options.path);
+ }
+ if (options.domain) {
+ sCookie += '; domain=' + (options.domain);
+ }
+ if (options.secure) {
+ sCookie += '; secure';
+ }
+ document.cookie = sCookie;
+}
+
+/**
+ * Get the value of a cookie with the given name.
+ *
+ * @param {String} sName: Unique name of a cookie (letters, numbers, underscores)
+ * @returns {String|null} The string value stored in a cookie
+ */
+function getCookie(sName) {
+ var sRE = '(?:; )?' + sName + '=([^;]*);?';
+ var oRE = new RegExp(sRE);
+ if (oRE.test(document.cookie)) {
+ return decodeURIComponent(RegExp['$1']);
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Delete cookie with given name
+ *
+ * @param {String} sName: Unique name of a cookie (letters, numbers, underscores)
+ * @param {Object} [options] An object literal containing key/value pairs
+ * to provide optional cookie attributes.
+ * @param {String} [options.path] Must be the same as when setting a cookie
+ * @param {String} [options.domain] Must be the same as when setting a cookie
+ */
+function deleteCookie(sName, options) {
+ options = options || {};
+ options.expires = 'delete';
+
+ setCookie(sName, '', options);
+}
+
+/* end of cookies.js */
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview Detect if JavaScript is enabled, and pass it to server-side
+ * @license GPLv2 or later
+ */
+
+
+/* ============================================================ */
+/* Manipulating links */
+
+/**
+ * used to check if link has 'js' query parameter already (at end),
+ * and other reasons to not add 'js=1' param at the end of link
+ * @constant
+ */
+var jsExceptionsRe = /[;?]js=[01](#.*)?$/;
+
+/**
+ * Add '?js=1' or ';js=1' to the end of every link in the document
+ * that doesn't have 'js' query parameter set already.
+ *
+ * Links with 'js=1' lead to JavaScript version of given action, if it
+ * exists (currently there is only 'blame_incremental' for 'blame')
+ *
+ * To be used as `window.onload` handler
+ *
+ * @globals jsExceptionsRe
+ */
+function fixLinks() {
+ var allLinks = document.getElementsByTagName("a") || document.links;
+ for (var i = 0, len = allLinks.length; i < len; i++) {
+ var link = allLinks[i];
+ if (!jsExceptionsRe.test(link)) {
+ link.href = link.href.replace(/(#|$)/,
+ (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1$1');
+ }
+ }
+}
+
+/* end of javascript-detection.js */
+// Copyright (C) 2011, John 'Warthog9' Hawley
+// 2011, Jakub Narebski
+
+/**
+ * @fileOverview Manipulate dates in gitweb output, adjusting timezone
+ * @license GPLv2 or later
+ */
+
+/**
+ * Get common timezone, add UI for changing timezones, and adjust
+ * dates to use requested common timezone.
+ *
+ * This function is called during onload event (added to window.onload).
+ *
+ * @param {String} tzDefault: default timezone, if there is no cookie
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @param {String} tzCookieInfo.name: name of cookie to store timezone
+ * @param {String} tzClassName: denotes elements with date to be adjusted
+ */
+function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) {
+ var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo);
+ var tz = tzDefault;
+
+ if (tzCookieTZ) {
+ // set timezone to value saved in a cookie
+ tz = tzCookieTZ;
+ // refresh cookie, so its expiration counts from last use of gitweb
+ setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo);
+ }
+
+ // add UI for changing timezone
+ addChangeTZ(tz, tzCookieInfo, tzClassName);
+
+ // server-side of gitweb produces datetime in UTC,
+ // so if tz is 'utc' there is no need for changes
+ var nochange = tz === 'utc';
+
+ // adjust dates to use specified common timezone
+ fixDatetimeTZ(tz, tzClassName, nochange);
+}
+
+
+/* ...................................................................... */
+/* Changing dates to use requested timezone */
+
+/**
+ * Replace RFC-2822 dates contained in SPAN elements with tzClassName
+ * CSS class with equivalent dates in given timezone.
+ *
+ * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local'
+ * @param {String} tzClassName: specifies elements to be changed
+ * @param {Boolean} nochange: markup for timezone change, but don't change it
+ */
+function fixDatetimeTZ(tz, tzClassName, nochange) {
+ // sanity check, method should be ensured by common-lib.js
+ if (!document.getElementsByClassName) {
+ return;
+ }
+
+ // translate to timezone in '(-|+)HHMM' format
+ tz = normalizeTimezoneInfo(tz);
+
+ // NOTE: result of getElementsByClassName should probably be cached
+ var classesFound = document.getElementsByClassName(tzClassName, "span");
+ for (var i = 0, len = classesFound.length; i < len; i++) {
+ var curElement = classesFound[i];
+
+ curElement.title = 'Click to change timezone';
+ if (!nochange) {
+ // we use *.firstChild.data (W3C DOM) instead of *.innerHTML
+ // as the latter doesn't always work everywhere in every browser
+ var epoch = parseRFC2822Date(curElement.firstChild.data);
+ var adjusted = formatDateRFC2882(epoch, tz);
+
+ curElement.firstChild.data = adjusted;
+ }
+ }
+}
+
+
+/* ...................................................................... */
+/* Adding triggers, generating timezone menu, displaying and hiding */
+
+/**
+ * Adds triggers for UI to change common timezone used for dates in
+ * gitweb output: it marks up and/or creates item to click to invoke
+ * timezone change UI, creates timezone UI fragment to be attached,
+ * and installs appropriate onclick trigger (via event delegation).
+ *
+ * @param {String} tzSelected: pre-selected timezone,
+ * 'utc' or 'local' or '(-|+)HHMM'
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @param {String} tzClassName: specifies elements to install trigger
+ */
+function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) {
+ // make link to timezone UI discoverable
+ addCssRule('.'+tzClassName + ':hover',
+ 'text-decoration: underline; cursor: help;');
+
+ // create form for selecting timezone (to be saved in a cookie)
+ var tzSelectFragment = document.createDocumentFragment();
+ tzSelectFragment = createChangeTZForm(tzSelectFragment,
+ tzSelected, tzCookieInfo, tzClassName);
+
+ // event delegation handler for timezone selection UI (clicking on entry)
+ // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/
+ // assumes that there is no existing document.onclick handler
+ document.onclick = function onclickHandler(event) {
+ //IE doesn't pass in the event object
+ event = event || window.event;
+
+ //IE uses srcElement as the target
+ var target = event.target || event.srcElement;
+
+ switch (target.className) {
+ case tzClassName:
+ // don't display timezone menu if it is already displayed
+ if (tzSelectFragment.childNodes.length > 0) {
+ displayChangeTZForm(target, tzSelectFragment);
+ }
+ break;
+ } // end switch
+ };
+}
+
+/**
+ * Create DocumentFragment with UI for changing common timezone in
+ * which dates are shown in.
+ *
+ * @param {DocumentFragment} documentFragment: where attach UI
+ * @param {String} tzSelected: default (pre-selected) timezone
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @returns {DocumentFragment}
+ */
+function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) {
+ var div = document.createElement("div");
+ div.className = 'popup';
+
+ /* '
X
' */
+ var closeButton = document.createElement('div');
+ closeButton.className = 'close-button';
+ closeButton.title = '(click on this box to close)';
+ closeButton.appendChild(document.createTextNode('X'));
+ closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName);
+ div.appendChild(closeButton);
+
+ /* 'Select timezone: ' */
+ div.appendChild(document.createTextNode('Select timezone: '));
+ var br = document.createElement('br');
+ br.clear = 'all';
+ div.appendChild(br);
+
+ /* '' */
+ var select = document.createElement("select");
+ select.name = "tzoffset";
+ //select.style.clear = 'all';
+ select.appendChild(generateTZOptions(tzSelected));
+ select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName);
+ div.appendChild(select);
+
+ documentFragment.appendChild(div);
+
+ return documentFragment;
+}
+
+
+/**
+ * Hide (remove from DOM) timezone change UI, ensuring that it is not
+ * garbage collected and that it can be re-enabled later.
+ *
+ * @param {DocumentFragment} documentFragment: contains detached UI
+ * @param {HTMLSelectElement} target: select element inside of UI
+ * @param {String} tzClassName: specifies element where UI was installed
+ * @returns {DocumentFragment} documentFragment
+ */
+function removeChangeTZForm(documentFragment, target, tzClassName) {
+ // find containing element, where we appended timezone selection UI
+ // `target' is somewhere inside timezone menu
+ var container = target.parentNode, popup = target;
+ while (container &&
+ container.className !== tzClassName) {
+ popup = container;
+ container = container.parentNode;
+ }
+ // safety check if we found correct container,
+ // and if it isn't deleted already
+ if (!container || !popup ||
+ container.className !== tzClassName ||
+ popup.className !== 'popup') {
+ return documentFragment;
+ }
+
+ // timezone selection UI was appended as last child
+ // see also displayChangeTZForm function
+ var removed = popup.parentNode.removeChild(popup);
+ if (documentFragment.firstChild !== removed) { // the only child
+ // re-append it so it would be available for next time
+ documentFragment.appendChild(removed);
+ }
+ // all of inline style was added by this script
+ // it is not really needed to remove it, but it is a good practice
+ container.removeAttribute('style');
+
+ return documentFragment;
+}
+
+
+/**
+ * Display UI for changing common timezone for dates in gitweb output.
+ * To be used from 'onclick' event handler.
+ *
+ * @param {HTMLElement} target: where to install/display UI
+ * @param {DocumentFragment} tzSelectFragment: timezone selection UI
+ */
+function displayChangeTZForm(target, tzSelectFragment) {
+ // for absolute positioning to be related to target element
+ target.style.position = 'relative';
+ target.style.display = 'inline-block';
+
+ // show/display UI for changing timezone
+ target.appendChild(tzSelectFragment);
+}
+
+
+/* ...................................................................... */
+/* List of timezones for timezone selection menu */
+
+/**
+ * Generate list of timezones for creating timezone select UI
+ *
+ * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' }
+ */
+function generateTZList() {
+ var timezones = [
+ { value: "utc", descr: "UTC/GMT"},
+ { value: "local", descr: "Local (per browser)"}
+ ];
+
+ // generate all full hour timezones (no fractional timezones)
+ for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) {
+ var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2);
+ timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'};
+ if (x === 0) {
+ timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC±00:00'
+ }
+ }
+
+ return timezones;
+}
+
+/**
+ * Generate elements for timezone select UI
+ *
+ * @param {String} tzSelected: default timezone
+ * @returns {DocumentFragment} list of options elements to appendChild
+ */
+function generateTZOptions(tzSelected) {
+ var elems = document.createDocumentFragment();
+ var timezones = generateTZList();
+
+ for (var i = 0, len = timezones.length; i < len; i++) {
+ var tzone = timezones[i];
+ var option = document.createElement("option");
+ if (tzone.value === tzSelected) {
+ option.defaultSelected = true;
+ }
+ option.value = tzone.value;
+ option.appendChild(document.createTextNode(tzone.descr));
+
+ elems.appendChild(option);
+ }
+
+ return elems;
+}
+
+
+/* ...................................................................... */
+/* Event handlers and/or their generators */
+
+/**
+ * Create event handler that select timezone and closes timezone select UI.
+ * To be used as $('select[name="tzselect"]').onchange handler.
+ *
+ * @param {DocumentFragment} tzSelectFragment: timezone selection UI
+ * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
+ * @param {String} tzCookieInfo.name: name of cookie to save result of selection
+ * @param {String} tzClassName: specifies element where UI was installed
+ * @returns {Function} event handler
+ */
+function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) {
+ //return function selectTZ(event) {
+ return function (event) {
+ event = event || window.event;
+ var target = event.target || event.srcElement;
+
+ var selected = target.options.item(target.selectedIndex);
+ removeChangeTZForm(tzSelectFragment, target, tzClassName);
+
+ if (selected) {
+ selected.defaultSelected = true;
+ setCookie(tzCookieInfo.name, selected.value, tzCookieInfo);
+ fixDatetimeTZ(selected.value, tzClassName);
+ }
+ };
+}
+
+/**
+ * Create event handler that closes timezone select UI.
+ * To be used e.g. as $('.closebutton').onclick handler.
+ *
+ * @param {DocumentFragment} tzSelectFragment: timezone selection UI
+ * @param {String} tzClassName: specifies element where UI was installed
+ * @returns {Function} event handler
+ */
+function closeTZFormHandler(tzSelectFragment, tzClassName) {
+ //return function closeTZForm(event) {
+ return function (event) {
+ event = event || window.event;
+ var target = event.target || event.srcElement;
+
+ removeChangeTZForm(tzSelectFragment, target, tzClassName);
+ };
+}
+
+/* end of adjust-timezone.js */
+// Copyright (C) 2007, Fredrik Kuivinen
+// 2007, Petr Baudis
+// 2008-2011, Jakub Narebski
+
+/**
+ * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb
+ * @license GPLv2 or later
+ */
+
+/* ============================================================ */
+/*
+ * This code uses DOM methods instead of (nonstandard) innerHTML
+ * to modify page.
+ *
+ * innerHTML is non-standard IE extension, though supported by most
+ * browsers; however Firefox up to version 1.5 didn't implement it in
+ * a strict mode (application/xml+xhtml mimetype).
+ *
+ * Also my simple benchmarks show that using elem.firstChild.data =
+ * 'content' is slightly faster than elem.innerHTML = 'content'. It
+ * is however more fragile (text element fragment must exists), and
+ * less feature-rich (we cannot add HTML).
+ *
+ * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the
+ * equivalent using DOM 2 Core is usually shown in comments.
+ */
+
+
+/* ............................................................ */
+/* utility/helper functions (and variables) */
+
+var projectUrl; // partial query + separator ('?' or ';')
+
+// 'commits' is an associative map. It maps SHA1s to Commit objects.
+var commits = {};
+
+/**
+ * constructor for Commit objects, used in 'blame'
+ * @class Represents a blamed commit
+ * @param {String} sha1: SHA-1 identifier of a commit
+ */
+function Commit(sha1) {
+ if (this instanceof Commit) {
+ this.sha1 = sha1;
+ this.nprevious = 0; /* number of 'previous', effective parents */
+ } else {
+ return new Commit(sha1);
+ }
+}
+
+/* ............................................................ */
+/* progress info, timing, error reporting */
+
+var blamedLines = 0;
+var totalLines = '???';
+var div_progress_bar;
+var div_progress_info;
+
+/**
+ * Detects how many lines does a blamed file have,
+ * This information is used in progress info
+ *
+ * @returns {Number|String} Number of lines in file, or string '...'
+ */
+function countLines() {
+ var table =
+ document.getElementById('blame_table') ||
+ document.getElementsByTagName('table')[0];
+
+ if (table) {
+ return table.getElementsByTagName('tr').length - 1; // for header
+ } else {
+ return '...';
+ }
+}
+
+/**
+ * update progress info and length (width) of progress bar
+ *
+ * @globals div_progress_info, div_progress_bar, blamedLines, totalLines
+ */
+function updateProgressInfo() {
+ if (!div_progress_info) {
+ div_progress_info = document.getElementById('progress_info');
+ }
+ if (!div_progress_bar) {
+ div_progress_bar = document.getElementById('progress_bar');
+ }
+ if (!div_progress_info && !div_progress_bar) {
+ return;
+ }
+
+ var percentage = Math.floor(100.0*blamedLines/totalLines);
+
+ if (div_progress_info) {
+ div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines +
+ ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)';
+ }
+
+ if (div_progress_bar) {
+ //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;');
+ div_progress_bar.style.width = percentage + '%';
+ }
+}
+
+
+var t_interval_server = '';
+var cmds_server = '';
+var t0 = new Date();
+
+/**
+ * write how much it took to generate data, and to run script
+ *
+ * @globals t0, t_interval_server, cmds_server
+ */
+function writeTimeInterval() {
+ var info_time = document.getElementById('generating_time');
+ if (!info_time || !t_interval_server) {
+ return;
+ }
+ var t1 = new Date();
+ info_time.firstChild.data += ' + (' +
+ t_interval_server + ' sec server blame_data / ' +
+ (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)';
+
+ var info_cmds = document.getElementById('generating_cmd');
+ if (!info_time || !cmds_server) {
+ return;
+ }
+ info_cmds.firstChild.data += ' + ' + cmds_server;
+}
+
+/**
+ * show an error message alert to user within page (in progress info area)
+ * @param {String} str: plain text error message (no HTML)
+ *
+ * @globals div_progress_info
+ */
+function errorInfo(str) {
+ if (!div_progress_info) {
+ div_progress_info = document.getElementById('progress_info');
+ }
+ if (div_progress_info) {
+ div_progress_info.className = 'error';
+ div_progress_info.firstChild.data = str;
+ }
+}
+
+/* ............................................................ */
+/* coloring rows during blame_data (git blame --incremental) run */
+
+/**
+ * used to extract N from 'colorN', where N is a number,
+ * @constant
+ */
+var colorRe = /\bcolor([0-9]*)\b/;
+
+/**
+ * return N if
, otherwise return null
+ * (some browsers require CSS class names to begin with letter)
+ *
+ * @param {HTMLElement} tr: table row element to check
+ * @param {String} tr.className: 'class' attribute of tr element
+ * @returns {Number|null} N if tr.className == 'colorN', otherwise null
+ *
+ * @globals colorRe
+ */
+function getColorNo(tr) {
+ if (!tr) {
+ return null;
+ }
+ var className = tr.className;
+ if (className) {
+ var match = colorRe.exec(className);
+ if (match) {
+ return parseInt(match[1], 10);
+ }
+ }
+ return null;
+}
+
+var colorsFreq = [0, 0, 0];
+/**
+ * return one of given possible colors (currently least used one)
+ * example: chooseColorNoFrom(2, 3) returns 2 or 3
+ *
+ * @param {Number[]} arguments: one or more numbers
+ * assumes that 1 <= arguments[i] <= colorsFreq.length
+ * @returns {Number} Least used color number from arguments
+ * @globals colorsFreq
+ */
+function chooseColorNoFrom() {
+ // choose the color which is least used
+ var colorNo = arguments[0];
+ for (var i = 1; i < arguments.length; i++) {
+ if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) {
+ colorNo = arguments[i];
+ }
+ }
+ colorsFreq[colorNo-1]++;
+ return colorNo;
+}
+
+/**
+ * given two neighbor
elements, find color which would be different
+ * from color of both of neighbors; used to 3-color blame table
+ *
+ * @param {HTMLElement} tr_prev
+ * @param {HTMLElement} tr_next
+ * @returns {Number} color number N such that
+ * colorN != tr_prev.className && colorN != tr_next.className
+ */
+function findColorNo(tr_prev, tr_next) {
+ var color_prev = getColorNo(tr_prev);
+ var color_next = getColorNo(tr_next);
+
+
+ // neither of neighbors has color set
+ // THEN we can use any of 3 possible colors
+ if (!color_prev && !color_next) {
+ return chooseColorNoFrom(1,2,3);
+ }
+
+ // either both neighbors have the same color,
+ // or only one of neighbors have color set
+ // THEN we can use any color except given
+ var color;
+ if (color_prev === color_next) {
+ color = color_prev; // = color_next;
+ } else if (!color_prev) {
+ color = color_next;
+ } else if (!color_next) {
+ color = color_prev;
+ }
+ if (color) {
+ return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1);
+ }
+
+ // neighbors have different colors
+ // THEN there is only one color left
+ return (3 - ((color_prev + color_next) % 3));
+}
+
+/* ............................................................ */
+/* coloring rows like 'blame' after 'blame_data' finishes */
+
+/**
+ * returns true if given row element (tr) is first in commit group
+ * to be used only after 'blame_data' finishes (after processing)
+ *
+ * @param {HTMLElement} tr: table row
+ * @returns {Boolean} true if TR is first in commit group
+ */
+function isStartOfGroup(tr) {
+ return tr.firstChild.className === 'sha1';
+}
+
+/**
+ * change colors to use zebra coloring (2 colors) instead of 3 colors
+ * concatenate neighbor commit groups belonging to the same commit
+ *
+ * @globals colorRe
+ */
+function fixColorsAndGroups() {
+ var colorClasses = ['light', 'dark'];
+ var linenum = 1;
+ var tr, prev_group;
+ var colorClass = 0;
+ var table =
+ document.getElementById('blame_table') ||
+ document.getElementsByTagName('table')[0];
+
+ while ((tr = document.getElementById('l'+linenum))) {
+ // index origin is 0, which is table header; start from 1
+ //while ((tr = table.rows[linenum])) { // <- it is slower
+ if (isStartOfGroup(tr, linenum, document)) {
+ if (prev_group &&
+ prev_group.firstChild.firstChild.href ===
+ tr.firstChild.firstChild.href) {
+ // we have to concatenate groups
+ var prev_rows = prev_group.firstChild.rowSpan || 1;
+ var curr_rows = tr.firstChild.rowSpan || 1;
+ prev_group.firstChild.rowSpan = prev_rows + curr_rows;
+ //tr.removeChild(tr.firstChild);
+ tr.deleteCell(0); // DOM2 HTML way
+ } else {
+ colorClass = (colorClass + 1) % 2;
+ prev_group = tr;
+ }
+ }
+ var tr_class = tr.className;
+ tr.className = tr_class.replace(colorRe, colorClasses[colorClass]);
+ linenum++;
+ }
+}
+
+
+/* ============================================================ */
+/* main part: parsing response */
+
+/**
+ * Function called for each blame entry, as soon as it finishes.
+ * It updates page via DOM manipulation, adding sha1 info, etc.
+ *
+ * @param {Commit} commit: blamed commit
+ * @param {Object} group: object representing group of lines,
+ * which blame the same commit (blame entry)
+ *
+ * @globals blamedLines
+ */
+function handleLine(commit, group) {
+ /*
+ This is the structure of the HTML fragment we are working
+ with:
+
+
+ */
+
+ var resline = group.resline;
+
+ // format date and time string only once per commit
+ if (!commit.info) {
+ /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */
+ commit.info = commit.author + ', ' +
+ formatDateISOLocal(commit.authorTime, commit.authorTimezone);
+ }
+
+ // color depends on group of lines, not only on blamed commit
+ var colorNo = findColorNo(
+ document.getElementById('l'+(resline-1)),
+ document.getElementById('l'+(resline+group.numlines))
+ );
+
+ // loop over lines in commit group
+ for (var i = 0; i < group.numlines; i++, resline++) {
+ var tr = document.getElementById('l'+resline);
+ if (!tr) {
+ break;
+ }
+ /*
+
+ This server serves coffees as proposed in famous
+ RFC 2324.
+ If you want one, simply send a HTTP (or HTTPS) request with BREW method using curl.
+
+
+ curl -X BREW http://git.dpolakovic.space
+
+
+ Your coffee will be brewed in one of my two coffee pots. Red and white one.
+ You can also pick coffee pot by name, while typing the the URL.
+ You can use their canonical names from the RFC (pot-0 and pot-1) or their
+ verbose variants (red-coffee-pot and white-coffee-pot). [*]
+
+
+ Before specifying the pot in BREW request, make sure it's not being cleaned or empty
+ by making a request with GET method.
+
+
+ curl -X GET http://git.dpolakovic.space/white-coffee-pot
+
+
+ If your chosen coffee pot is empty, you have to wait until the server
+ refills the coffee beans or you can refill them yourself with POST method.
+
+
+ curl -X POST http://git.dpolakovic.space/white-coffee-pot
+
+
+ If you preffer coffee with some additions, you can include them in
+ your request. If they are available, the server will happily prepare
+ your order.
+
+ As seen in the last example, the WHEN method is substitued by quantity ordering.
+ You don't have to hold the connection open until your milk is poured. Simply
+ declare how much milk (or any other addition) you would like to have in your coffee.
+
+
+ This server offers updated list of additions, to fit modern coffee trends.
+ The original RFC didn't specified plant based milk types like pea milk or
+ other addition types like
+ nowadays very popular pumpkin puree or cinnamon.
+ Complete offer of additions and coffee pots can be found with PROPFIND method.
+
+
+ curl -X PROPFIND http://git.dpolakovic.space
+
+
+ If you are not currenttly on Unix or Unix-like system, you can request your coffee
+ using telnet.
+
+ I even made a perl script which offers
+ simplified way to request coffee from any machine. You can find it right
+ here on this git server as
+ htcpcp-requestor.
+
+
+ The counter you see stores BREW method request from past 48 hours and list
+ all from midnight of your timezone. Link to the source code is available
+ also
+ here
+ on this git server.
+
+
+ I was also thinking about implementing the
+ Coke protocol,
+ but I don't own any soda vending machine. If you have one and you
+ are willing to donate it for the good of the Internet, please send me
+ an email.
+
+
+
+
+ * Quick tought about coffee pot numbering
+ The RFC 2324 defines the canonical pot name in format "pot-[integer]". This
+ is confusing as there is no first known member of integer group.
+ There is however first known member of integer data type which is 0, but the
+ mathematical first member of the type could also be -2,147,483,648.
+ On the other hand, most natural number asigned to first member of any group
+ (even group of coffee pots) which is also member of integer group is 1.
+ Therefore,
+ the red coffee pot has working aliases: pot-2 (since the pot-0 and pot-1
+ redirecting to same pot could evoke bug behaviour), pot--2147483648 and
+ pot-n2147483648.
+ The white coffee pot is then pot-1, even tough it is not the default option.
+ To avoid any possible confusion from numbering coffee pots, I also implemented
+ their human readable color based names, which is present throughout this guide.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Git-server/Stuff/contributors-guide.html b/Git-server/Stuff/contributors-guide.html
new file mode 100644
index 0000000..e19ab7b
--- /dev/null
+++ b/Git-server/Stuff/contributors-guide.html
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+ Contributor's guide
+
+
+
+
+
+
+
+
+ This guide will show you everything you need to do, in order to contribute a project.
+ It is primarily oriented towards users of Unix or Unix-like operating systems,
+ but I tested it on Windows running Git bash and it worked just fine.
+
+ Contributors perform clone and push to the server using ssh key.
+ The key has to be ed25519 type in RFC 4716 format. You can generate one with this
+ command:
+
+ Use email address which you will add later in your git config.
+
+
+
+ Privacy consideration: this email address will be visible on web in log
+ and shortlog.
+
+
+
+ Use long passphrase and store your keys securely. If you use older ssh package,
+ you might end up with key in Open SSH format. In that case, convert the public key into
+ desired format with command:
+
+ Copy the newly generated/converted public ssh key contents into an email. With it,
+ name projects you want to contribute to, so I can add you correct access rights.
+ As soon as I reply to your email, you can start contributing.
+
+
+ In meantime, you can modify your ssh config file.
+
+
+ nano ~/.ssh/config
+
+
+
+ Note: you can actually use any text editor you preffer, I put nano
+ here because it is default editor in Windows Git Bash and it's
+ favorite editor of my good friend Jakub.
+
+
+
+ Add these lines:
+
+
+
+Host git.dpolakovic.space
+ HostName git.dpolakovic.space
+ User git
+ Port 1388
+ IdentityFile ~/.ssh/ed25519_git_dpolakovic_space
+ IdentitiesOnly yes
+
+
+
+
Git setup
+
+ If you already obtained the confirmation reply, you can clone projects
+ using this command structure:
+
+ You can test your setup on testing repository:
+
+
+ git clone git@git.dpolakovic.space:testing
+
+
+ Enter the directory and create your branch:
+
+
+ git checkout -b "my-new-branch"
+
+
+ If you haven't setup your global git config name and email, it's
+ good time to set it up now. Please use the email you entered
+ for ssh-keygen before.
+
+ You will be prompted to enter your ssh key passphrase. Submit it and
+ your changes should be there. Confirm it by visiting the
+ testing repository.
+
+
+
+ Note: if your email has gravatar account, your gravatar profile picture
+ will be visible on this server too.
+
+
+ Q: How can I leave the server?
+ A: Just send me an email with this request and I will delete your ssh key.
+
+
+ Q: Can I become a collaborator?
+ A: Sadly no.
+
+
+ Q: Could you mirror a project on different git server?
+ A: Yes. I preffer codeberg or notabug.org, but I am willing to mirror
+ on other places too, if you wish so.
+
+
+ Q: Can I host my own project on this server?
+ A: Well, generally no, but it depends on the project. Since it's
+ unusual request, send me an email.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Git-server/Stuff/dot.png b/Git-server/Stuff/dot.png
new file mode 100644
index 0000000..c4c7217
Binary files /dev/null and b/Git-server/Stuff/dot.png differ
diff --git a/Git-server/Stuff/server-rules.html b/Git-server/Stuff/server-rules.html
new file mode 100644
index 0000000..0b6a6f6
--- /dev/null
+++ b/Git-server/Stuff/server-rules.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ Server rules
+
+
+
+
+
+
+
+
+ These rules are expected to be followed by contributors on this server.
+ It's a mix of some good practices, my preferences and server setup. I
+ hope you won't find them very restrictive as I sincerely welcome everyone
+ who wants to help me.
+
+
+
+
+
+
+ A contributor may participate in up to two projetcs at once,
+ excluding the testing project.
+
+
+ Every commit must include a type and subject. Type must be one of:
+ feat, docs, chore, style, or perf. Subject should be
+ concise and descriptive.
+
+
+ Larger commits must include changes in bullet points.
+ Each point should describe a distinct change.
+
+
+ Always check if your changes don't colide with current project license.
+
+
+ No contributor is allowed to push project license changes.
+
+
+ Resolve potential conflicts before asking for merge.
+
+
+ When contributing to project from category "2 - scripts", try to avoid
+ using external libraries.
+
+
+ Don't change structure declared in README.txt, especially
+ in projects from category "1 - programs".
+
+
+ It's strictly forbidden to change README.txt file to README.md.
+
+
+ Comment the code. Use signle line comment describing what it does.
+ If necessary, include second line about how it's done and/or third line
+ explaining weird choices you decided to implement.
+
+
+ Try to avoid lines longer than 90 characters.
+
+
+ No editor-specific
+ pushes are allowed (like incluson of #files generated by Emacs).
+
+
+ In project related (or any) email communication use inline replies.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Git-server/Stuff/stuff.css b/Git-server/Stuff/stuff.css
new file mode 100644
index 0000000..1e5b57c
--- /dev/null
+++ b/Git-server/Stuff/stuff.css
@@ -0,0 +1,121 @@
+body {
+ font-family: sans-serif;
+ font-size: 14px;
+ border: solid #d9d8d1;
+ border-width: 1px;
+ /* margin: 10px; */
+
+ width:1000px;
+ margin: 0 auto;
+ margin-bottom: 20px;
+ background-color: #ffffff;
+ color: #000000;
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+main {
+ padding-left: 13px;
+ padding-top: 10px;
+ width: 790px;
+}
+
+a {
+ color: #0000cc;
+}
+
+a:hover, a:visited, a:active {
+ color: #551a8b;
+}
+
+a:hover {
+ color: #4682b4;
+}
+
+html {
+ background-image: url("https://dpolakovic.space/Pictures/sky.jpg")
+}
+
+div.page_header a:visited, a.header {
+ color: #0000cc;
+}
+
+.banner {
+ width: 100%;
+ margin: 0 auto;
+ max-width: 1000px;
+ padding-left: 2px;
+ padding-top: 3px;
+}
+
+.banner img {
+ max-width: 100%;
+}
+
+.banner a {
+ text-decoration: none;
+
+}
+
+div.page_header {
+ height: 25px;
+ padding: 8px;
+ font-size: 150%;
+ font-weight: bold;
+ background-color: rgb(217, 216, 209);
+}
+
+div.page_footer {
+ height: 22px;
+ padding: 4px 8px;
+ background-color: #d9d8d1;
+}
+
+div.page_footer_text {
+ line-height: 22px;
+ float: left;
+ color: #555555;
+ font-style: italic;
+}
+
+.center {
+ padding-left: 380px;
+}
+
+footer {
+ padding-top: 15px;
+ padding-bottom: 20px;
+ text-align: center;
+ font-size: small;
+}
+
+code {
+ display: inline;
+ padding: 2px 6px;
+ margin: 0;
+ font-family: 'Courier New', Consolas, monospace;
+ background-color: #f4f4f4;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ font-size: 0.9em;
+ color: #333;
+ white-space: nowrap;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+
+.code-block {
+ display: block;
+ width: 750px;
+ padding: 2px 6px;
+ margin: 0;
+ font-family: 'Courier New', Consolas, monospace;
+ background-color: #f4f4f4;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+ font-size: 0.9em;
+ color: #333;
+ white-space: pre-wrap;
+ padding-left: 15px;
+ padding-right: 15px;
+}
diff --git a/Git-server/Stuff/stuff.js b/Git-server/Stuff/stuff.js
new file mode 100644
index 0000000..41ab88d
--- /dev/null
+++ b/Git-server/Stuff/stuff.js
@@ -0,0 +1,69 @@
+/**
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2025 David Polakovic
+ *
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this page.
+ *
+*/
+
+const dotImg = document.getElementById("toggleImageDot");
+const spaceImg = document.getElementById("toggleImageSpace");
+const dpoImg = document.getElementById("toggleImagedpo");
+
+// Mouse behavior for dot
+if (dotImg) {
+ dotImg.addEventListener("mousedown", () => {
+ dotImg.src = "https://dpolakovic.space/Pictures/dot2.png";
+ });
+ dotImg.addEventListener("mouseup", () => {
+ dotImg.src = "https://dpolakovic.space/Pictures/dot.png";
+ });
+ dotImg.addEventListener("mouseleave", () => {
+ dotImg.src = "https://dpolakovic.space/Pictures/dot.png";
+ });
+}
+
+// Mouse behavior for space
+if (spaceImg) {
+ spaceImg.addEventListener("mousedown", () => {
+ spaceImg.src = "https://dpolakovic.space/Pictures/space2.png";
+ });
+ spaceImg.addEventListener("mouseup", () => {
+ spaceImg.src = "https://dpolakovic.space/Pictures/space.png";
+ });
+ spaceImg.addEventListener("mouseleave", () => {
+ spaceImg.src = "https://dpolakovic.space/Pictures/space.png";
+ });
+}
+
+// Mouse behavior for dpo
+if (dpoImg) {
+ dpoImg.addEventListener("mousedown", () => {
+ dpoImg.src = "https://dpolakovic.space/Pictures/dpo2.png";
+ });
+ dpoImg.addEventListener("mouseup", () => {
+ dpoImg.src = "https://dpolakovic.space/Pictures/dpolakovic.png";
+ });
+ dpoImg.addEventListener("mouseleave", () => {
+ dpoImg.src = "https://dpolakovic.space/Pictures/dpolakovic.png";
+ });
+}
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..36d2e89
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,158 @@
+================================================================================
+ dpolakovic.space
+ website files for dpolakovic.space and all it's subdomains
+================================================================================
+
+AUTHOR : David Polakovic
+EMAIL : email@dpolakovic.space
+TXT VERSION : 1.0.0
+LAST UPDATED: 2026-02-24
+LICENSE : GPLv3, with exception of /Git-server/Gtiweb folder
+
+--------------------------------------------------------------------------------
+TABLE OF CONTENTS
+--------------------------------------------------------------------------------
+
+ 1. Overview
+ 2. Project structure
+ 3. Requirements
+ 4. Installation
+ 5. Usage
+ 6. Reference
+ 7. Configuration
+ 8. Examples
+ 9. Troubleshooting
+ 10. License
+
+--------------------------------------------------------------------------------
+1. OVERVIEW
+--------------------------------------------------------------------------------
+
+ This file describes everything I host on my server.
+
+--------------------------------------------------------------------------------
+2. PROJECT STRUCTURE
+--------------------------------------------------------------------------------
+
+ dpolakovic-space
+ âââ Git-server ## files from git.dpolakovic.space
+ â  âââ Coffee ## RFC 2324 implementation
+ â  â  âââ coffee-counter.js # counts served coffees from 48h ago
+ â  â  âââ coffee-server.php # custom http methods definitions
+ â  âââ Gitweb ## gitweb (front page) files
+ â  â  âââ static
+ â  â  â  âââ gitweb.css
+ â  â  â  âââ gitweb.js
+ â  â  âââ banner.html # contains JS for clicky images
+ â  â  âââ footer.html # contains JS which counts coffees
+ â  â  âââ gitweb.cgi # front page logic customization
+ â  â  âââ indextext.html # contains styles for front page
+ â  âââ Stuff ## other pages present on git server
+ â  âââ coffee-server.html
+ â  âââ contributors-guide.html
+ â  âââ dot.png
+ â  âââ server-rules.html
+ â  âââ stuff.css
+ â  âââ stuff.js
+ â
+ âââ Root-domain ## files from www.dpolakovic.space
+ â  âââ blogs ## contains all blog pages
+ â  âââ error
+ â  â  âââ error.php # custom http status code handler
+ â  âââ php
+ â  â  âââ config.php # logic file included in all pages
+ â  â  âââ leaderboard.php # logic file for leaderboard of CTF
+ â  âââ Pictures
+ â  âââ Styles
+ â  â  âââ FONT_LICENSE.txt
+ â  â  âââ NotoSerif-Bold.ttf
+ â  â  âââ NotoSerif-Italic.ttf
+ â  â  âââ NotoSerif-Regular.ttf
+ â  â  âââ styles.css
+ â  âââ brew.php # implementation of RFC 2324
+ â  âââ clicky-images.js
+ â  âââ example-page.php
+ â
+ âââ Tor-site ## files from Tor site
+ â  âââ Dead-drops ## encrypted messages from dead drop
+ â  âââ php
+ â  â  âââ captcha.php # captcha for dead drops
+ â  â  âââ config.php # config for all pages
+ â  â  âââ dd2.php # dead drop logic
+ â  âââ Pictures
+ â  âââ Styles
+ â  â  âââ FONT_LICENSE.txt
+ â  â  âââ NotoSerif-Bold.ttf
+ â  â  âââ NotoSerif-Italic.ttf
+ â  â  âââ NotoSerif-Regular.ttf
+ â  â  âââ styles.css
+ â  âââ dead-drop.php # dead drop front page
+ â  âââ example-page.php
+ â
+ âââ LICENSE.txt
+ âââ README.txt
+
+
+--------------------------------------------------------------------------------
+3. REQUIREMENTS
+--------------------------------------------------------------------------------
+
+ - web server (tested on Apache)
+ - PHP (any version)
+ - git (any version < 3.0.0)
+ - gitweb (any version)
+
+--------------------------------------------------------------------------------
+4. INSTALLATION
+--------------------------------------------------------------------------------
+
+ 1. Clone or download this repository.
+
+ 2. Copy any file you want into your server.
+
+--------------------------------------------------------------------------------
+5. USAGE
+--------------------------------------------------------------------------------
+
+ -
+
+--------------------------------------------------------------------------------
+6. REFERENCE
+--------------------------------------------------------------------------------
+
+ Short references are written in project structure.
+
+--------------------------------------------------------------------------------
+7. CONFIGURATION
+--------------------------------------------------------------------------------
+
+ -
+
+--------------------------------------------------------------------------------
+8. EXAMPLES
+--------------------------------------------------------------------------------
+
+ -
+
+--------------------------------------------------------------------------------
+9. TROUBLESHOOTING
+--------------------------------------------------------------------------------
+
+ Problem : Coffee counter shows error.
+ Solution: Restart the web server. This generates new json files and usually
+ solves this
+
+ Problem : Dead drops don't show contents.
+ Solution: Check permissions and ownership on Dead-drops/ folder. It should
+ be apache:apache
+
+--------------------------------------------------------------------------------
+10. LICENSE
+--------------------------------------------------------------------------------
+
+ This project is licensed under the GPLv3 License.
+ See the LICENSE.txt file for full details.
+
+ Copyright (c) 2026 David Polakovic
+
+================================================================================
diff --git a/Root-domain/Styles/FONT_LICENSE.txt b/Root-domain/Styles/FONT_LICENSE.txt
new file mode 100644
index 0000000..6843f31
--- /dev/null
+++ b/Root-domain/Styles/FONT_LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/Root-domain/Styles/NotoSerif-Bold.ttf b/Root-domain/Styles/NotoSerif-Bold.ttf
new file mode 100755
index 0000000..aa21d44
Binary files /dev/null and b/Root-domain/Styles/NotoSerif-Bold.ttf differ
diff --git a/Root-domain/Styles/NotoSerif-Italic.ttf b/Root-domain/Styles/NotoSerif-Italic.ttf
new file mode 100755
index 0000000..786ecc7
Binary files /dev/null and b/Root-domain/Styles/NotoSerif-Italic.ttf differ
diff --git a/Root-domain/Styles/NotoSerif-Regular.ttf b/Root-domain/Styles/NotoSerif-Regular.ttf
new file mode 100755
index 0000000..dc30297
Binary files /dev/null and b/Root-domain/Styles/NotoSerif-Regular.ttf differ
diff --git a/Root-domain/Styles/styles.css b/Root-domain/Styles/styles.css
new file mode 100755
index 0000000..fa30641
--- /dev/null
+++ b/Root-domain/Styles/styles.css
@@ -0,0 +1,776 @@
+/* Styles.css
+ this is a file for setting visual styles of the web page
+ I am no webdesigner so the comments are rather simple
+ copyright 2023, David Polakovic, licensed under GPLv3, see README
+*/
+
+
+/* set both background and font color to keep content readable for
+ users with different text colors in browser settings.
+*/
+
+
+/* I love this font. */
+@font-face {
+ font-family: 'Noto Serif';
+ src: url('/Styles/NotoSerif-Regular.ttf') format('truetype');
+ font-weight: 450;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Noto Serif';
+ src: url('/Styles/NotoSerif-Bold.ttf') format('truetype');
+ font-weight: 700;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Noto Serif';
+ src: url('/Styles/NotoSerif-Italic.ttf') format('truetype');
+ font-weight: normal;
+ font-style: italic;
+}
+
+body {
+ background-color: #E8CBA9;
+ color: #000000;
+ font-family: 'Noto Serif', serif; /* Set font to Noto Serif */
+ font-size: 18px;
+}
+
+table {
+ border-collapse: collapse;
+ border: 1px solid grey;
+ width: 100%;
+}
+
+th {
+ background-color: #c69879;
+ border: 1px solid grey;
+}
+
+td {
+ padding: 2px;
+ border: 1px solid grey;
+}
+
+tr:hover {
+ background-color: #efdac3;
+}
+
+.center {
+ text-align: center;
+}
+
+.my-filter {
+ text-align: center;
+ padding-bottom: 50px;
+}
+
+/* Navigation bar
+ just simple bar from W3 schools with minor edits
+ https://www.w3schools.com/Css/css_navbar_horizontal.asp
+*/
+
+nav {
+ display: flex;
+}
+.nav-bar > ul {
+list-style-type: none;
+max-width: 1000px;
+width: 100%;
+margin: 0 auto;
+padding: 0;
+background-color: #333333;
+border-radius: 10px;
+}
+
+.nav-bar li {
+float: left;
+position: relative; /* Added for dropdown positioning */
+}
+.nav-bar li a {
+display: block;
+color: white;
+text-align: center;
+padding: 14px 18px;
+text-decoration: none;
+}
+.nav-bar li:last-child {
+float: right;
+position: relative;
+}
+.nav-bar li:last-child a {
+border-radius: 0 14px 14px 0;
+}
+.gitserver {
+background-color: #1e1e1e;
+}
+.gitstatus {
+display: none;
+list-style-type: none;
+position: absolute;
+color: black;
+}
+.nav-bar li:last-child:hover .gitstatus {
+display: block;
+}
+
+/* Dropdown styles */
+.dropdown {
+display: block;
+color: white;
+text-align: center;
+text-decoration: none;
+cursor: pointer;
+}
+
+/* Hide the checkbox */
+.dropdown input[type="checkbox"] {
+display: none;
+}
+
+/* Style the label like the dropdown text */
+.dropdown label {
+display: block;
+color: white;
+text-align: center;
+padding: 14px 16px;
+text-decoration: none;
+cursor: pointer;
+}
+
+.dropdown-content {
+display: none;
+position: absolute;
+background-color: #333333;
+min-width: 160px;
+z-index: 1;
+top: 100%;
+left: 0;
+border-radius: 0 0 10px 10px;
+list-style-type: none;
+padding: 0;
+}
+
+.dropdown-content li {
+float: none;
+width: 100%;
+}
+
+.dropdown-content a {
+color: white;
+padding: 12px 16px;
+text-decoration: none;
+display: block;
+text-align: left;
+}
+.dropdown-content a:hover {
+}
+.dropdown-content a:last-child {
+border-radius: 0 0 10px 10px;
+}
+
+/* Show dropdown when checkbox is checked */
+.dropdown input[type="checkbox"]:checked ~ .dropdown-content {
+display: block;
+}
+
+/* Keep the hover functionality for desktop */
+.dropdown:hover .dropdown-content {
+display: block;
+}
+.dropdown:hover {
+}
+
+/*
+.gitstatus li::before {
+ content: '';
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ position: absolute;
+ top: 50%;
+ left: -14px;
+ transform: translateY(-50%);
+ background-color: green;
+}
+
+.gitstatus.offline li:before {
+ background-color: red;
+}
+*/
+
+/* Mobile burger menu styles */
+.mobile-menu-toggle {
+ display: none;
+}
+
+.mobile-menu-icon {
+ display: none;
+ cursor: pointer;
+ padding: 14px 16px;
+ color: white;
+ font-size: 24px;
+}
+
+/* Banner pictures
+ style pictures shown on top of every page - should be all same size,
+ but just in case
+*/
+.banner {
+ width: 100%;
+ margin: 0 auto;
+ max-width: 1000px;
+}
+
+.banner img {
+ max-width: 100%;
+}
+
+/* when pictures are made into links, they show little dash in right corner
+ of the picture - this deletes that
+*/
+.banner a {
+ text-decoration: none;
+
+}
+
+/* Main content
+ style for main content of each page - centers text and other stuff
+*/
+.content {
+ max-width: 975px;
+ height: auto;
+ width: 100%;
+ margin: 0 auto;
+ text-align: justify;
+ line-height: 1.25;
+}
+
+.less {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ margin: 0;
+}
+
+.content img {
+ max-width: 100%;
+}
+
+.content img.bee-gifs {
+ max-width: 105px;
+}
+
+.hide-link {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+/* Footer
+ Simple footer inspired by navigation bar - changed text to be white
+*/
+.footer {
+ max-width: 980px;
+ width: auto;
+ margin: 0 auto;
+ margin-bottom: 2em;
+ padding: 10px;
+ text-align: center;
+ background-color: #b38265;
+ font-size: 13px;
+ color: white;
+ border-radius: 10px;
+
+}
+
+/* changes colors of links to white in footer */
+.footer a {
+ color: white;
+}
+
+/* this is class for my public GPG key fetch - only used once */
+.text-area {
+ font-family: monospace;
+}
+
+/* this is class for anybrowser button to appear far right */
+.gif-buttons {
+ float: right;
+ line-height: 1;
+ margin-left: 5px; /* Add 10px space to the left of each button */s
+}
+
+.dir {
+ margin-left: 10px;
+}
+
+/* Blog previews
+ these are clases for my blog page - they show small picture and
+ description in small fonts
+*/
+.blog-prev {
+ display: flex;
+}
+
+.blog-prev-img {
+ width: 150px;
+ height: 95px;
+ float: left;
+}
+
+.blog-prev-img img {
+ width: 150px;
+ height: 95px;
+ object-fit: cover;
+
+}
+
+.blog-prev-text {
+ margin-left: 10px;
+ height: 95px;
+ float: left;
+ color: #545454;
+}
+
+.fresh-link:visited {
+ color: rgb(0, 0, 238);
+}
+
+.filter:link,
+.filter:visited {
+ color: rgb(0, 0, 238);
+}
+
+.filter.active {
+ color: red;
+}
+
+/* Navigation bar for blog page
+ So here is modified nav bar with only one option (Go back)
+ I put it as a secound style, because I don't want last child
+ to be pushed on the far right side (as it was for the "Git server"
+*/
+
+.nav-bar-blog {}
+
+.nav-bar-blog ul {
+ list-style-type: none;
+ max-width:1000px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0;
+ overflow: hidden;
+ background-color: #333;
+ border-radius: 10px;
+}
+
+/* make everything stick to left side */
+.nav-bar-blog li {
+ float: left;
+}
+
+/* style for contents of the nav bar */
+.nav-bar-blog li a {
+ display: block;
+ color: white;
+ text-align: center;
+ padding: 14px 16px;
+ text-decoration: none;
+}
+
+
+/* Block of code
+ This is what I use for block of code formating
+*/
+
+.code-block {
+ background: #efdac3;
+ border: 1px solid #ddd;
+ border-left: 3px solid #b38265;
+ color: #666;
+ page-break-inside: avoid;
+ font-family: monospace;
+ font-size: 15px;
+ line-height: 1.6;
+ margin-bottom: 1.6em;
+ max-width: 100%;
+ overflow-x: auto; /* Enable horizontal scrolling */
+ white-space: nowrap; /* Prevent text wrapping */
+ padding: 1em 1.5em;
+ display: block;
+ word-wrap: break-word;
+}
+
+.code-block2 {
+ background: #efdac3;
+ border: 1px solid #ddd;
+ border-left: 3px solid #b38265;
+ color: #666;
+ page-break-inside: avoid;
+ font-family: monospace;
+ font-size: 15px;
+ line-height: 1.6;
+ margin-bottom: 1.6em;
+ max-width: 100%;
+ overflow-x: auto; /* Enable horizontal scrolling */
+ white-space: nowrap; /* Prevent text wrapping */
+ padding: 1em 1.5em;
+ display: block;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+
+
+.terminal-block {
+ background: #000;
+ border: 1px solid #000;
+ /*border-left: 3px solid #f36d33; */
+ border-left: 3px solid #000;
+ color: white;
+ /* color: #99ffe6;
+ color: #80ffdf; */
+ font-weight: bold;
+ page-break-inside: avoid;
+ font-family: monospace;
+ font-size: 15px;
+ line-height: 1.6;
+ margin-bottom: 1.6em;
+ max-width: 100%;
+ overflow-x: auto; /* Enable horizontal scrolling */
+ white-space: nowrap; /* Prevent text wrapping */
+ padding: 1em 1.5em;
+ display: block;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.correspondence-block {
+ background: #f5f5f0;
+ border: 1px solid #ddd;
+ border-left: 3px solid #4a90e2;
+ color: #333;
+ page-break-inside: avoid;
+ font-family: 'Times New Roman', Times, serif;
+ font-size: 18px;
+ line-height: 1.3;
+ margin-bottom: 1em;
+ max-width: 100%;
+ overflow-x: auto;
+ padding: 0.5em 1.5em;
+ display: block;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.correspondence-block .header {
+ border-bottom: 1px solid #999;
+ /*padding-bottom: 0.5em;*/
+ margin-bottom: 0.75em;
+ margin-left: 1.9em;
+ font-weight: normal;
+ width: 600px;
+ max-width: 100%;
+}
+
+.what-is-this {
+ max-width: 975px;
+ height: auto;
+ width: 100%;
+ margin: 0 auto;
+ text-align: justify;
+ color: #e8cba9;
+}
+
+.what-is-this a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+.error-block {
+ color: #973232;
+ width: 630px;
+ margin: 0 auto;
+}
+
+.error-block a {
+ color: #c53333;
+}
+
+.desktop-only {
+ display: block; /* Visible on desktop */
+}
+
+/* ============================================
+ MOBILE RESPONSIVE STYLES
+ ============================================
+ everything from down here is AI mumbo jumbo
+ because I struggle to find anything more
+ dumb than the resposive css. I hate doing this.
+ I did it after 3 years of having this web just
+ because lot o people were spamming my mailbox
+ that "the site looks ugly on their phone".
+ Well I did it, here u are, fuck you and fuck css.
+ */
+
+@media screen and (max-width: 768px) {
+
+ /* Prevent any horizontal scroll or overflow */
+ html, body {
+ overflow-x: hidden;
+ width: 100%;
+ max-width: 100%;
+ margin: 0;
+ padding: 0;
+ font-size: 15px;
+ }
+
+ * {
+ max-width: 100%;
+ box-sizing: border-box;
+ }
+
+ /* Hide banner completely on mobile */
+ .banner {
+ display: none;
+ }
+
+ /* Mobile navigation - full width, no rounded corners, touching top */
+ .mobile-menu-icon {
+ display: block;
+ background-color: #333333;
+ margin: 0;
+ text-align: left;
+ width: 100%;
+ border-radius: 0;
+ padding: 19px 10px;
+ }
+
+ .mobile-menu-icon::after {
+ content: "â¼ dpolakovic.space";
+ font-size: 16px;
+ }
+
+ nav {
+ display: block;
+ width: 100%;
+ margin-bottom: 2em;
+ }
+
+ .nav-bar > ul {
+ display: none;
+ max-width: 100%;
+ width: 100%;
+ border-radius: 0;
+ margin: 0;
+ }
+
+ .mobile-menu-toggle:checked ~ ul {
+ display: block;
+ }
+
+ .nav-bar li {
+ float: none;
+ width: 100%;
+ border-bottom: 1px solid #444;
+ }
+
+ .nav-bar li a {
+ text-align: left;
+ padding: 14px 15px;
+ padding-left: 30px;
+ border-radius: 0;
+ font-size: 16px;
+ }
+
+ /* All ul/li text should be left-aligned on mobile */
+ ul li {
+ text-align: left;
+ }
+
+ /* Blog navigation bar - match the Menu label exactly with asymmetric padding */
+ .nav-bar-blog {
+ margin: 0;
+ }
+
+ .nav-bar-blog ul {
+ max-width: 100%;
+ width: 100%;
+ border-radius: 0;
+ margin: 0;
+ margin-bottom: 2em;
+ padding: 0;
+ list-style: none;
+ }
+
+ .nav-bar-blog li {
+ float: none;
+ width: 100%;
+ margin: 0;
+ }
+
+ .nav-bar-blog li a {
+ display: block;
+ background-color: #333333;
+ margin: 0;
+ text-align: left;
+ width: 100%;
+ border-radius: 0;
+ padding-top: 30px;
+ padding-bottom: 25px;
+ padding-left: 20px;
+ padding-right: 20px;
+ font-size: 16px;
+ color: white;
+ text-decoration: none;
+ line-height: 16px;
+ }
+
+ .nav-bar li:last-child {
+ float: none;
+ border-bottom: none;
+ }
+
+ .nav-bar li:last-child a {
+ border-radius: 0;
+ }
+
+ /* Git server with dark background stays at bottom of menu */
+ .gitserver {
+ background-color: #1e1e1e;
+ }
+
+ /* Left-align text for blog posts */
+ body.blog-post .content {
+ text-align: left;
+ }
+
+ /* Glider button stays in content on mobile */
+ .content .gif-buttons {
+ display: none;
+ }
+
+ .content img.bee-gifs {
+ margin: 0 auto;
+ width: 75px;
+ height: 75px;
+ }
+
+ /* Main content */
+ .content {
+ max-width: 100%;
+ padding: 5px 12px;
+ margin: 0;
+ box-sizing: border-box;
+ }
+
+ .content h2 {
+ margin-top: 0;
+ padding-left: 0;
+ }
+
+ .content p {
+ padding-left: 0;
+ margin-left: 0;
+ }
+
+ /* Center class - make links left-aligned and stacked on mobile, but keep images centered */
+ .center {
+ text-align: left;
+ white-space: normal;
+ padding-left: 1.5em; /* Space for bullets */
+ margin: 0;
+ }
+
+ .center a {
+ display: list-item;
+ list-style-type: disc; /* Or 'circle', 'square', 'none' */
+ margin-bottom: 0.5em;
+ }
+
+ .center img {
+ display: block;
+ text-align: center;
+ margin: 0 auto;
+ }
+
+ /* All links should be left-aligned on mobile */
+ a {
+ text-align: left;
+ }
+
+ /* Footer full width, edge to edge, stick to bottom */
+ .footer {
+ max-width: 100%;
+ width: 100%;
+ border-radius: 0;
+ text-align: left;
+ padding: 15px 12px;
+ padding-bottom: 55px;
+ margin: 0;
+ position: relative;
+ }
+
+ /* Hide what-is-this section on mobile */
+ .what-is-this {
+ display: none;
+ }
+
+ /* Error block responsive */
+ .error-block {
+ width: 100%;
+ padding: 15px 20px;
+ }
+
+ /* Blog previews stack on mobile */
+ .blog-prev {
+ flex-direction: column;
+ }
+
+ .blog-prev-img {
+ display: none;
+ }
+
+ .my-filter {
+ display: none;
+ }
+
+
+
+
+ .blog-prev-text {
+ margin-left: 15px;
+ margin-top: 5px;
+ height: auto;
+ /* margin-bottom: 8px; */
+ }
+
+ /* Code blocks responsive */
+ .code-block,
+ .terminal-block {
+ font-size: 13px;
+ padding: 0.8em 1em;
+ }
+
+ /* Tables responsive - horizontally scrollable */
+ table {
+ font-size: 14px;
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ th, td {
+ padding: 6px 4px;
+ }
+
+ .desktop-only {
+ display: none;
+ }
+}
+
+/* styles.css */
\ No newline at end of file
diff --git a/Root-domain/brew.php b/Root-domain/brew.php
new file mode 100644
index 0000000..7dd05a2
--- /dev/null
+++ b/Root-domain/brew.php
@@ -0,0 +1,14 @@
+
diff --git a/Root-domain/clicky-images.js b/Root-domain/clicky-images.js
new file mode 100644
index 0000000..fec6bc9
--- /dev/null
+++ b/Root-domain/clicky-images.js
@@ -0,0 +1,95 @@
+
+/**
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2025 David Polakovic
+ *
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this page.
+ *
+ */
+
+const dotImg = document.getElementById("toggleImageDot");
+const spaceImg = document.getElementById("toggleImageSpace");
+const dpoImg = document.getElementById("toggleImagedpo");
+
+// Mouse behavior for dot
+if (dotImg) {
+ dotImg.addEventListener("mousedown", () => {
+ dotImg.src = "../Pictures/dot2.png";
+ });
+ dotImg.addEventListener("mouseup", () => {
+ dotImg.src = "../Pictures/dot.png";
+ });
+ dotImg.addEventListener("mouseleave", () => {
+ dotImg.src = "../Pictures/dot.png";
+ });
+}
+
+// Mouse behavior for space
+if (spaceImg) {
+ spaceImg.addEventListener("mousedown", () => {
+ spaceImg.src = "../Pictures/space2.png";
+ });
+ spaceImg.addEventListener("mouseup", () => {
+ spaceImg.src = "../Pictures/space.png";
+ });
+ spaceImg.addEventListener("mouseleave", () => {
+ spaceImg.src = "../Pictures/space.png";
+ });
+}
+
+// Mouse behavior for dpo
+if (dpoImg) {
+ dpoImg.addEventListener("mousedown", () => {
+ dpoImg.src = "../Pictures/dpo2.png";
+ });
+ dpoImg.addEventListener("mouseup", () => {
+ dpoImg.src = "../Pictures/dpolakovic.png";
+ });
+ dpoImg.addEventListener("mouseleave", () => {
+ dpoImg.src = "../Pictures/dpolakovic.png";
+ });
+}
+
+// Keyboard behavior
+document.addEventListener("keydown", (event) => {
+ if (dotImg && event.key === ".") {
+ dotImg.src = "../Pictures/dot2.png";
+ }
+ if (spaceImg && event.key === " ") {
+ spaceImg.src = "../Pictures/space2.png";
+ }
+ if (dpoImg && event.key === "d") {
+ dpoImg.src = "../Pictures/dpo2.png";
+ }
+});
+
+document.addEventListener("keyup", (event) => {
+ if (dotImg && event.key === ".") {
+ dotImg.src = "../Pictures/dot.png";
+ }
+ if (spaceImg && event.key === " ") {
+ spaceImg.src = "../Pictures/space.png";
+ }
+ if (dpoImg && event.key === "d") {
+ dpoImg.src = "../Pictures/dpolakovic.png";
+ }
+});
\ No newline at end of file
diff --git a/Root-domain/error/error.php b/Root-domain/error/error.php
new file mode 100644
index 0000000..4a4599f
--- /dev/null
+++ b/Root-domain/error/error.php
@@ -0,0 +1,308 @@
+ [
+ 'title' => '100: Continue',
+ 'message' => 'Begin you may. Ready to receive more, the server is.'
+ ],
+ '101' => [
+ 'title' => '101: Switching Protocols',
+ 'message' => 'Switch protocols, we shall. Agreed to your upgrade request, the server has.'
+ ],
+ '102' => [
+ 'title' => '102: Processing',
+ 'message' => 'Processing your request, we are. Patience you must have. Time, this will take.'
+ ],
+ '103' => [
+ 'title' => '103: Early Hints',
+ 'message' => 'Early hints, I give you. While the response prepares, preload resources you can.'
+ ],
+
+ // *******
+ // * 200 *
+ // *******
+ '200' => [
+ 'title' => '200: OK',
+ 'message' => 'What you sought, received you have.'
+ ],
+ '201' => [
+ 'title' => '201: Created',
+ 'message' => 'Created, a new resource has been.'
+ ],
+ '202' => [
+ 'title' => '202: Accepted',
+ 'message' => 'Accepted for processing, your request is. Complete yet, it is not. Patience, you must have.'
+ ],
+ '203' => [
+ 'title' => '203: Non-Authoritative Information',
+ 'message' => 'Information you receive, but from the source directly, it comes not. Trust it carefully, you should.'
+ ],
+ '204' => [
+ 'title' => '204: No Content',
+ 'message' => 'Successful, your request was. But content to show you, there is none.'
+ ],
+ '205' => [
+ 'title' => '205: Reset Content',
+ 'message' => 'Reset your view, you must. Successful the request was, but refresh your document, you should.'
+ ],
+ '206' => [
+ 'title' => '206: Partial Content',
+ 'message' => 'Only part of what you requested, delivered it is. The rest, await it does.'
+ ],
+ '207' => [
+ 'title' => '207: Multi-Status',
+ 'message' => 'Multiple operations performed, we have. Different outcomes, each one has.'
+ ],
+ '208' => [
+ 'title' => '208: Already Reported',
+ 'message' => 'Already reported in this response, these results were. Repeat them, we will not.'
+ ],
+ '226' => [
+ 'title' => '226: IM Used',
+ 'message' => 'Instance manipulations applied, we have. Transformed, the resource is.'
+ ],
+ // *******
+ // * 300 *
+ // *******
+ '300' => [
+ 'title' => '300: Multiple Choices',
+ 'message' => 'Many paths before you lie. Choose wisely, you must.'
+ ],
+ '301' => [
+ 'title' => '301: Moved Permanently',
+ 'message' => 'Moved, this page has. Forever gone from here, it is. Follow the new path, you must.'
+ ],
+ '302' => [
+ 'title' => '302: Found',
+ 'message' => 'Found elsewhere, what you seek is. Temporarily moved, it has. Follow, you should.'
+ ],
+ '303' => [
+ 'title' => '303: See Other',
+ 'message' => 'Look elsewhere, you must. Another location, the answer awaits.'
+ ],
+ '304' => [
+ 'title' => '304: Not Modified',
+ 'message' => 'Changed, nothing has. Your cached version, still good it is.'
+ ],
+ '305' => [
+ 'title' => '305: Use Proxy',
+ 'message' => 'Through a proxy, your path must go. Direct access, allowed it is not.'
+ ],
+
+ // *******
+ // * 400 *
+ // *******
+ '400' => [
+ 'title' => '400: Bad Request',
+ 'message' => 'Understand your request, I cannot. Malformed it is, hmm.'
+ ],
+ '401' => [
+ 'title' => '401: Unauthorized',
+ 'message' => 'Identify yourself, you must. Access without credentials, forbidden it is.'
+ ],
+ '402' => [
+ 'title' => '402: Payment Required',
+ 'message' => 'Pay first, you must. Free, this resource is not'
+ ],
+ '403' => [
+ 'title' => '403: Forbidden',
+ 'message' => 'Permission to enter, you have not. Forbidden this path is.'
+ ],
+ '404' => [
+ 'title' => '404: Not Found',
+ 'message' => 'The page you seek, exist does not. Lost in the void, it is.'
+ ],
+ '405' => [
+ 'title' => '405: Method Not Allowed',
+ 'message' => 'You must unlearn what you have learned. This method, the way it is not.'
+ ],
+ '406' => [
+ 'title' => '406: Not Acceptable',
+ 'message' => 'Difficult to see. Always in motion, acceptable formats are. Yours, compatible is not.'
+ ],
+ '407' => [
+ 'title' => '407: Proxy Authentication Required',
+ 'message' => 'Through the proxy, your journey goes. But authenticate you must, before pass you may.'
+ ],
+ '408' => [
+ 'title' => '408: Request Timeout',
+ 'message' => 'Too long you have taken. Patience, the server has lost, hmm.'
+ ],
+ '409' => [
+ 'title' => '409: Conflict',
+ 'message' => 'Fear leads to anger. Anger leads to conflict. With current state, your request conflicts.'
+ ],
+ '410' => [
+ 'title' => '410: Gone',
+ 'message' => 'Once here it was. Gone forever now, it is.'
+ ],
+ '411' => [
+ 'title' => '411: Length Required',
+ 'message' => 'Size matters not... but to the server, it does. Specify the length, you must.'
+ ],
+ '412' => [
+ 'title' => '412: Precondition Failed',
+ 'message' => 'Met, your preconditions were not. Failed, the requirements have.'
+ ],
+ '413' => [
+ 'title' => '413: Content Too Large',
+ 'message' => 'Too large, your request is. Smaller, you must make it.'
+ ],
+ '414' => [
+ 'title' => '414: URI Too Long',
+ 'message' => 'Too long, your URL is. Shorten it, you must.'
+ ],
+ '415' => [
+ 'title' => '415: Unsupported Media Type',
+ 'message' => 'Much to learn, you still have. This format, support the server does not.'
+ ],
+ '416' => [
+ 'title' => '416: Range Not Satisfiable',
+ 'message' => 'Only what you take with you. Beyond the resource bounds, your range reaches.'
+ ],
+ '417' => [
+ 'title' => '417: Expectation Failed',
+ 'message' => 'Meet your expectations, the server cannot. Expect less, perhaps you should.'
+ ],
+ '418' => [
+ 'title' => '418: I\'m a teapot',
+ 'message' => 'A teapot, I am. Brew coffee, I cannot. Absurd this is, yes, hmmm!'
+ ],
+ '421' => [
+ 'title' => '421: Misdirected Request',
+ 'message' => 'Wrong server, you have reached. Directed elsewhere, you should be.'
+ ],
+ '422' => [
+ 'title' => '422: Unprocessable Content',
+ 'message' => 'Understand your request, I do. Process it correctly, I cannot.'
+ ],
+ '423' => [
+ 'title' => '423: Locked',
+ 'message' => 'Locked, this resource is. Access it now, you cannot.'
+ ],
+ '424' => [
+ 'title' => '424: Failed Dependency',
+ 'message' => 'Failed, a dependency has. Complete your request, I cannot.'
+ ],
+ '425' => [
+ 'title' => '425: Too Early',
+ 'message' => 'Hasty, you are. Patience, the Force requires. Too early, your request comes.'
+ ],
+ '426' => [
+ 'title' => '426: Upgrade Required',
+ 'message' => 'Upgrade your protocol, you must. Old version, no longer supported is.'
+ ],
+ '428' => [
+ 'title' => '428: Precondition Required',
+ 'message' => 'Conditional, your request must be. Preconditions, you must include.'
+ ],
+ '429' => [
+ 'title' => '429: Too Many Requests',
+ 'message' => 'Too eager you are. Slow down, you must. Many requests, too many!'
+ ],
+ '431' => [
+ 'title' => '431: Request Header Fields Too Large',
+ 'message' => 'Train yourself to let go of everything you fear to lose. Too large, your headers are. Release some, you must.'
+ ],
+ '451' => [
+ 'title' => '451: Unavailable For Legal Reasons',
+ 'message' => 'The dark side of the Force surrounds the law. By legal reasons, blocked this is. Available, it is not.'
+ ],
+
+ // *******
+ // * 500 *
+ // *******
+
+ '500' => [
+ 'title' => '500: Internal Server Error',
+ 'message' => 'Wrong, something has gone. Know what it is, the server does not. Mysterious this error is.'
+ ],
+ '501' => [
+ 'title' => '501: Not Implemented',
+ 'message' => 'Implement this feature, the server does not. Capable of it, it is not.'
+ ],
+ '502' => [
+ 'title' => '502: Bad Gateway',
+ 'message' => 'The gateway, troubled it is. A response invalid, received it has.'
+ ],
+ '503' => [
+ 'title' => '503: Service Unavailable',
+ 'message' => 'Available, the server is not. Overwhelmed or resting, it may be. Return later, you should.'
+ ],
+ '504' => [
+ 'title' => '504: Gateway Timeout',
+ 'message' => 'For another server\'s response, waited too long I have. Answer me, it did not.'
+ ],
+ '505' => [
+ 'title' => '505: HTTP Version Not Supported',
+ 'message' => 'Your HTTP version, too old it is. Support it, the server cannot.'
+ ],
+ '506' => [
+ 'title' => '506: Variant Also Negotiates',
+ 'message' => 'Circular, the negotiation has become. A proper variant, choose we cannot.'
+ ],
+ '507' => [
+ 'title' => '507: Insufficient Storage',
+ 'message' => 'No more space, there is. Full, the storage has become.'
+ ],
+ '508' => [
+ 'title' => '508: Loop Detected',
+ 'message' => 'Around in circles, the server goes. Break free, we must.'
+ ],
+ '510' => [
+ 'title' => '510: Not Extended',
+ 'message' => 'Further extensions required are. Provided, they were not.'
+ ],
+ '511' => [
+ 'title' => '511: Network Authentication Required',
+ 'message' => 'Before proceed you may, authenticate with the network, you must. Required, it is.'
+ ],
+];
+
+if (isset($errors[$error_code])) {
+ $title = $errors[$error_code]['title'];
+ $message = $errors[$error_code]['message'];
+} else {
+ $title = $error_code . ': Is not a valid code';
+ $message = 'Stupid you are. Breed you should not.';
+}
+
+// Only set HTTP response code for 4xx and 5xx errors
+if ($error_code >= 400) {
+ http_response_code((int)$error_code);
+}
+
+// REMOVE THIS LINE - it was the duplicate causing the problem:
+// http_response_code((int)$error_code);
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Root-domain/example-page.php b/Root-domain/example-page.php
new file mode 100755
index 0000000..89e5024
--- /dev/null
+++ b/Root-domain/example-page.php
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ dpolakovic.space
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ttile
+
+ punch line
+
+
+ First paragraph.
+
+
+ Every other paragraph.
+
+
+
+
+
+
+
+
diff --git a/Root-domain/php/config.php b/Root-domain/php/config.php
new file mode 100755
index 0000000..0a818cc
--- /dev/null
+++ b/Root-domain/php/config.php
@@ -0,0 +1,255 @@
+";
+
+ $connection = @fsockopen($domain, $port, $errno, $errstr, 5);
+ if ($connection) {
+ fclose($connection);
+ echo "";
+ return true;
+ } else {
+ echo "";
+ return false;
+ }
+}
+
+// Show if git server is down or running
+function serverStatus() {
+ $websiteUrl = "http://git.dpolakovic.space";
+ if (isWebsiteOnline($websiteUrl)) {
+ echo 'Git server';
+ } else {
+ echo 'Git server | offline';
+ }
+}
+
+// Pick random second sentence on index.php
+function randomSentence() {
+ $file = "./php/rnd.txt";
+
+ // Read all lines from the file into an array
+ $lines = file($file, FILE_IGNORE_NEW_LINES);
+
+ if ($lines !== false && !empty($lines)) {
+
+ // Get a random line index
+ $randomIndex = array_rand($lines);
+
+ // Display the random line
+ echo $lines[$randomIndex];
+
+ }
+ else { echo "this is an error message."; }
+}
+
+// Print time of last update
+function lastFileUpdate(...$files) {
+ $latestTimestamp = 0;
+ foreach ($files as $file) {
+ if (file_exists($file)) {
+ $fileTime = filemtime($file);
+ if ($fileTime > $latestTimestamp) {
+ $latestTimestamp = $fileTime;
+ }
+ }
+ }
+ // Format and print the date
+ if ($latestTimestamp > 0) {
+ $formattedDate = date('d/m/Y', $latestTimestamp);
+ echo "$formattedDate";
+ } else {
+ echo "No file found";
+ }
+}
+
+// Counts lines...
+function countFileLines($filename) {
+ if (!file_exists($filename)) {
+ echo "File not found: $filename ";
+ return;
+ }
+
+ $lineCount = count(file($filename));
+ echo $lineCount;
+}
+
+// Print footer on every page
+function printFooter($usesJS, $usesCookies, ...$files) {
+ // Print JS status
+ echo "Copyright 2023-" . date("Y") . " David Polakovic. All publications licensed under ";
+ echo 'CC BY-SA 4.0 unless stated otherwise. ';
+ echo $usesJS ? 'This page uses trivial JavaScript' : 'This page is JavaScript free';
+ echo $usesCookies ? " and temporary PHPSESSID cookie. " : ". This page doesn't use cookies. ";
+ echo 'Source code available here under ';
+ echo 'GPLv3 license. ';
+ // Find the most recent modification date from the files
+ $latestTimestamp = 0;
+ foreach ($files as $file) {
+ if (file_exists($file)) {
+ $fileTime = filemtime($file);
+ if ($fileTime > $latestTimestamp) {
+ $latestTimestamp = $fileTime;
+ }
+ }
+ }
+ // Format and print the date
+ if ($latestTimestamp > 0) {
+ $formattedDate = date('d/m/Y', $latestTimestamp);
+ echo "Last update on: $formattedDate. ";
+ } else {
+ echo "Last update on: No valid files found ";
+ }
+ echo 'Server for this domain is ';
+ echo 'RFC 1464 and ';
+ echo 'RFC 2324 compliant.';
+}
+
+// Just like footer...
+function printNavBar(){
+ echo <<
+
+
+
\n");
+ fclose($file);
+
+ // Append the key_string to usedkeys.txt
+ file_put_contents($usedKeysFile, $key_string . "\n", FILE_APPEND | LOCK_EX);
+
+ $_SESSION['message'] = '';
+ header("location: ../6C6561646572626F617264.php");
+} else {
+ $_SESSION['message'] = "Ah ah ah, you didn't say the magic word!";
+ header("location: ../6C6561646572626F617264.php");
+}
+?>
+
+
diff --git a/Tor-site/Styles/FONT_LICENSE.txt b/Tor-site/Styles/FONT_LICENSE.txt
new file mode 100644
index 0000000..6843f31
--- /dev/null
+++ b/Tor-site/Styles/FONT_LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/Tor-site/Styles/NotoSerif-Bold.ttf b/Tor-site/Styles/NotoSerif-Bold.ttf
new file mode 100755
index 0000000..aa21d44
Binary files /dev/null and b/Tor-site/Styles/NotoSerif-Bold.ttf differ
diff --git a/Tor-site/Styles/NotoSerif-Italic.ttf b/Tor-site/Styles/NotoSerif-Italic.ttf
new file mode 100755
index 0000000..786ecc7
Binary files /dev/null and b/Tor-site/Styles/NotoSerif-Italic.ttf differ
diff --git a/Tor-site/Styles/NotoSerif-Regular.ttf b/Tor-site/Styles/NotoSerif-Regular.ttf
new file mode 100755
index 0000000..dc30297
Binary files /dev/null and b/Tor-site/Styles/NotoSerif-Regular.ttf differ
diff --git a/Tor-site/Styles/styles.css b/Tor-site/Styles/styles.css
new file mode 100755
index 0000000..126828c
--- /dev/null
+++ b/Tor-site/Styles/styles.css
@@ -0,0 +1,173 @@
+/* Define the font faces */
+@font-face {
+ font-family: 'Noto Serif';
+ src: url('/Styles/NotoSerif-Regular.ttf') format('truetype');
+ font-weight: 450;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Noto Serif';
+ src: url('/Styles/NotoSerif-Bold.ttf') format('truetype');
+ font-weight: 700;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Noto Serif';
+ src: url('/Styles/NotoSerif-Italic.ttf') format('truetype');
+ font-weight: normal;
+ font-style: italic;
+}
+
+body {
+ background-color: #E8CBA9;;
+ color: #000000;
+ font-family: 'Noto Serif', serif;
+ font-size: 18px;
+}
+
+table {
+ border-collapse: collapse;
+ border: 1px solid grey;
+ width: 100%;
+ font-size: 16px;
+}
+
+th {
+ background-color: #c69879;
+ border: 1px solid grey;
+}
+
+td {
+ padding: 2px;
+ border: 1px solid grey;
+}
+
+tr:hover {
+ background-color: #efdac3;
+}
+
+nav {
+ display: flex;
+}
+
+.nav-bar > ul {
+list-style-type: none;
+max-width: 1000px;
+width: 100%;
+margin: 0 auto;
+padding: 0;
+background-color: #333333;
+border-radius: 10px;
+}
+
+.nav-bar li {
+float: left;
+position: relative;
+}
+
+.nav-bar li a {
+display: block;
+color: white;
+text-align: center;
+padding: 14px 16px;
+text-decoration: none;
+}
+
+.title-with-icon {
+ display: flex;
+ align-items: center;
+ gap: 15px; /* space between text and image */
+}
+
+.banner {
+ width: 100%;
+ max-width: 1000px;
+ margin: 0 auto;
+ overflow: visible;
+}
+
+.banner p {
+ white-space: nowrap;
+ margin: 1em 0;
+ padding: 0;
+ overflow: visible;
+}
+
+.banner img {
+ max-width: none;
+ display: inline-block;
+}
+
+.content {
+ max-width: 975px;
+ height: auto;
+ width: 100%;
+ margin: 0 auto;
+ text-align: justify;
+ line-height: 1.25;
+}
+
+.less {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ margin: 0;
+}
+
+.content img {
+ max-width: 100%;
+}
+
+.hide-link {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+.footer {
+ max-width: 980px;
+ width: auto;
+ margin: 0 auto;
+ padding: 10px;
+ text-align: center;
+ background-color: #b38265;
+ font-size: 13px;
+ color: white;
+ border-radius: 10px;
+
+}
+
+.footer a {
+ color: white;
+}
+
+.text-area {
+ font-family: monospace;
+ white-space: pre;
+}
+
+.fresh-link:visited {
+ color: rgb(0, 0, 238);
+}
+
+.terminal-block {
+ background: #000;
+ border: 1px solid #000;
+ border-left: 3px solid #000;
+ color: white;
+ font-weight: bold;
+ page-break-inside: avoid;
+ font-family: monospace;
+ font-size: 15px;
+ line-height: 1.6;
+ margin-bottom: 1.6em;
+ max-width: 100%;
+ overflow-x: auto;
+ white-space: nowrap;
+ padding: 1em 1.5em;
+ display: block;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
diff --git a/Tor-site/dead-drop.php b/Tor-site/dead-drop.php
new file mode 100755
index 0000000..b47cf65
--- /dev/null
+++ b/Tor-site/dead-drop.php
@@ -0,0 +1,69 @@
+
+
+
+
+
+ Dead drop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dead drop
+ A technique used for anonymous information exchange.
+
+
+ Enter the coordinates of a dead drop (a place) to check if it has stashed message or not.
+ Contents of messages are encrypted with AES-256-CBC. The encryption key are the location coordinates itself.
+ Each location entry is then hashed with SHA-256 to hide it from server owner and
+ webmaster (me).
+
+
+
+
+
+
+
+
+
+ Every message gets automaticaly deleted after 3 days.
+